All components

Datetime Input

Datetime Input is a React and Tailwind CSS form component from UI Layouts, licensed MIT. Copy it and paste it into your project.

UI Layouts MIT forms

What it is

Datetime Input is a React and Tailwind CSS form component from UI Layouts, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i chrono-node@latest react-day-picker@^8.9.1

Licence

This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Datetime Input

./components/ui/datetime-input.tsx
// @ts-nocheck
'use client';

import { Button, buttonVariants } from '@/components/website/ui/button';
import { Calendar, type CalendarProps } from '@/components/website/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/website/ui/popover';
import { ScrollArea } from '@/components/website/ui/scroll-area';
import { cn } from '@/lib/utils';
import { parseDate } from 'chrono-node';
import { Calendar as CalendarIcon, LucideTextCursorInput } from 'lucide-react';
import React from 'react';
import type { ActiveModifiers } from 'react-day-picker';

/* -------------------------------------------------------------------------- */
/*                               Inspired By:                                 */
/*                               @steventey                                   */
/* ------------------https://dub.co/blog/smart-datetime-picker--------------- */
/* -------------------------------------------------------------------------- */

/**
 * Utility function that parses dates.
 * Parses a given date string using the `chrono-node` library.
 *
 * @param str - A string representation of a date and time.
 * @returns A `Date` object representing the parsed date and time, or `null` if the string could not be parsed.
 */
export const parseDateTime = (str: Date | string) => {
  if (str instanceof Date) return str;
  return parseDate(str);
};

/**
 * Converts a given timestamp or the current date and time to a string representation in the local time zone.
 * format: `HH:mm`, adjusted for the local time zone.
 *
 * @param timestamp {Date | string}
 * @returns A string representation of the timestamp
 */
export const getDateTimeLocal = (timestamp?: Date): string => {
  const d = timestamp ? new Date(timestamp) : new Date();
  if (d.toString() === 'Invalid Date') return '';
  return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
    .toISOString()
    .split(':')
    .slice(0, 2)
    .join(':');
};

/**
 * Formats a given date and time object or string into a human-readable string representation.
 * "MMM D, YYYY h:mm A" (e.g. "Jan 1, 2023 12:00 PM").
 *
 * @param datetime - {Date | string}
 * @returns A string representation of the date and time
 */
const formatTimeOnly = (datetime: Date | string) => {
  return new Date(datetime).toLocaleTimeString('en-US', {
    hour: 'numeric',
    minute: 'numeric',
    hour12: true,
  });
};

const formatDateOnly = (datetime: Date | string) => {
  return new Date(datetime).toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
  });
};

const formatDateTime = (
  datetime: Date | string,
  showCalendar: boolean,
  showTimePicker: boolean
) => {
  if (!showCalendar && showTimePicker) {
    return formatTimeOnly(datetime);
  }
  if (showCalendar && !showTimePicker) {
    return formatDateOnly(datetime);
  }
  return new Date(datetime).toLocaleTimeString('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    hour12: true,
  });
};

const inputBase =
  'bg-transparent focus:outline-hidden focus:ring-0 focus-within:outline-hidden focus-within:ring-0 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50';

// @source: https://www.perplexity.ai/search/in-javascript-how-RfI7fMtITxKr5c.V9Lv5KA#1
// use this pattern to validate the transformed date string for the natural language input
const naturalInputValidationPattern = '^[A-Z][a-z]{2}sd{1,2},sd{4},sd{1,2}:d{2}s[AP]M$';

const DEFAULT_SIZE = 96;

/**
 * Smart time input Docs: {@link: https://shadcn-extension.vercel.app/docs/smart-time-input}
 */

interface SmartDatetimeInputProps {
  value?: Date;
  onValueChange: (date: Date) => void;
  showCalendar?: boolean;
  showTimePicker?: boolean;
}

interface SmartDatetimeInputContextProps extends SmartDatetimeInputProps {
  Time: string;
  onTimeChange: (time: string) => void;
}

const SmartDatetimeInputContext = React.createContext<SmartDatetimeInputContextProps | null>(null);

const useSmartDateInput = () => {
  const context = React.useContext(SmartDatetimeInputContext);
  if (!context) {
    throw new Error('useSmartDateInput must be used within SmartDateInputProvider');
  }
  return context;
};
export const SmartDatetimeInput = React.forwardRef<
  HTMLInputElement,
  Omit<
    React.InputHTMLAttributes<HTMLInputElement>,
    'type' | 'ref' | 'value' | 'defaultValue' | 'onBlur'
  > &
    SmartDatetimeInputProps
>(
  (
    {
      className,
      value,
      onValueChange,
      placeholder,
      disabled,
      showCalendar = true,
      showTimePicker = true,
    },
    ref
  ) => {
    const [Time, setTime] = React.useState<string>('');

    const onTimeChange = React.useCallback((time: string) => {
      setTime(time);
    }, []);

    // If neither calendar nor timepicker is specified, show both
    const shouldShowBoth = showCalendar === showTimePicker;

    return (
      <SmartDatetimeInputContext.Provider
        value={{
          value,
          onValueChange,
          Time,
          onTimeChange,
          showCalendar: shouldShowBoth ? true : showCalendar,
          showTimePicker: shouldShowBoth ? true : showTimePicker,
        }}
      >
        <div className='flex items-center justify-center dark:bg-neutral-950 bg-neutral-50'>
          <div
            className={cn(
              'flex gap-1 w-full p-1 items-center justify-between rounded-md border-2 transition-all',
              'focus-within:outline-0 focus:outline-0 focus:ring-0',
              'placeholder:text-muted-foreground focus-visible:outline-0 ',
              className
            )}
          >
            <DateTimeLocalInput />
            <NaturalLanguageInput placeholder={placeholder} disabled={disabled} ref={ref} />
          </div>
        </div>
      </SmartDatetimeInputContext.Provider>
    );
  }
);

SmartDatetimeInput.displayName = 'DatetimeInput';

// Make it a standalone component

const TimePicker = () => {
  const { value, onValueChange, Time, onTimeChange } = useSmartDateInput();
  const [activeIndex, setActiveIndex] = React.useState(-1);
  const timestamp = 15;

  const formateSelectedTime = React.useCallback(
    (time: string, hour: number, partStamp: number) => {
      onTimeChange(time);

      const newVal = value ? new Date(value) : new Date();

      // If no value exists, use current date but only set the time
      newVal.setHours(hour, partStamp === 0 ? Number.parseInt('00') : timestamp * partStamp);

      onValueChange(newVal);
    },
    [value, onValueChange, onTimeChange]
  );

  const handleKeydown = React.useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      e.stopPropagation();

      if (!document) return;

      const moveNext = () => {
        const nextIndex = activeIndex + 1 > DEFAULT_SIZE - 1 ? 0 : activeIndex + 1;

        const currentElm = document.getElementById(`time-${nextIndex}`);

        currentElm?.focus();

        setActiveIndex(nextIndex);
      };

      const movePrev = () => {
        const prevIndex = activeIndex - 1 < 0 ? DEFAULT_SIZE - 1 : activeIndex - 1;

        const currentElm = document.getElementById(`time-${prevIndex}`);

        currentElm?.focus();

        setActiveIndex(prevIndex);
      };

      const setElement = () => {
        const currentElm = document.getElementById(`time-${activeIndex}`);

        if (!currentElm) return;

        currentElm.focus();

        const timeValue = currentElm.textContent ?? '';

        // this should work now haha that hour is what does the trick

        const PM_AM = timeValue.split(' ')[1];
        const PM_AM_hour = Number.parseInt(timeValue.split(' ')[0].split(':')[0]);
        const hour =
          PM_AM === 'AM'
            ? PM_AM_hour === 12
              ? 0
              : PM_AM_hour
            : PM_AM_hour === 12
              ? 12
              : PM_AM_hour + 12;

        const part = Math.floor(Number.parseInt(timeValue.split(' ')[0].split(':')[1]) / 15);

        formateSelectedTime(timeValue, hour, part);
      };

      const reset = () => {
        const currentElm = document.getElementById(`time-${activeIndex}`);
        currentElm?.blur();
        setActiveIndex(-1);
      };

      switch (e.key) {
        case 'ArrowUp':
          movePrev();
          break;

        case 'ArrowDown':
          moveNext();
          break;

        case 'Escape':
          reset();
          break;

        case 'Enter':
          setElement();
          break;
      }
    },
    [activeIndex, formateSelectedTime]
  );

  const handleClick = React.useCallback(
    (hour: number, part: number, PM_AM: string, currentIndex: number) => {
      formateSelectedTime(`${hour}:${part === 0 ? '00' : timestamp * part} ${PM_AM}`, hour, part);
      setActiveIndex(currentIndex);
    },
    [formateSelectedTime]
  );

  const currentTime = React.useMemo(() => {
    const timeVal = Time.split(' ')[0];
    return {
      hours: Number.parseInt(timeVal.split(':')[0]),
      minutes: Number.parseInt(timeVal.split(':')[1]),
    };
  }, [Time]);

  React.useEffect(() => {
    const getCurrentElementTime = () => {
      const timeVal = Time.split(' ')[0];
      const hours = Number.parseInt(timeVal.split(':')[0]);
      const minutes = Number.parseInt(timeVal.split(':')[1]);
      const PM_AM = Time.split(' ')[1];

      const formatIndex = PM_AM === 'AM' ? hours : hours === 12 ? hours : hours + 12;
      const formattedHours = formatIndex;

      console.log(formatIndex);

      for (let j = 0; j <= 3; j++) {
        const diff = Math.abs(j * timestamp - minutes);
        const selected =
          PM_AM === (formattedHours >= 12 ? 'PM' : 'AM') &&
          (minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp);

        if (selected) {
          const trueIndex = activeIndex === -1 ? formattedHours * 4 + j : activeIndex;

          setActiveIndex(trueIndex);

          const currentElm = document.getElementById(`time-${trueIndex}`);
          currentElm?.scrollIntoView({
            block: 'center',
            behavior: 'smooth',
          });
        }
      }
    };

    getCurrentElementTime();
  }, [Time, activeIndex]);

  const height = React.useMemo(() => {
    if (!document) return;
    const calendarElm = document.getElementById('calendar');
    if (!calendarElm) return;
    return calendarElm.style.height;
  }, []);

  return (
    <div className='space-y-2 pr-3 py-3 relative '>
      <h3 className='text-sm font-medium text-center'>Time</h3>
      <ScrollArea
        onKeyDown={handleKeydown}
        className='h-[90%] w-full focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-0 py-0.5'
        style={{
          height,
        }}
      >
        <ul className={cn('flex items-center flex-col gap-1 h-full max-h-56 w-28 px-1 py-0.5')}>
          {Array.from({ length: 24 }).map((_, i) => {
            const PM_AM = i >= 12 ? 'PM' : 'AM';
            const formatIndex = i > 12 ? i % 12 : i === 0 || i === 12 ? 12 : i;
            return Array.from({ length: 4 }).map((_, part) => {
              const diff = Math.abs(part * timestamp - currentTime.minutes);

              const trueIndex = i * 4 + part;

              // ? refactor : add the select of the default time on the current device (H:MM)
              const isSelected =
                (currentTime.hours === i || currentTime.hours === formatIndex) &&
                Time.split(' ')[1] === PM_AM &&
                (currentTime.minutes <= 53 ? diff < Math.ceil(timestamp / 2) : diff < timestamp);

              const isSuggested = !value && isSelected;

              const currentValue = `${formatIndex}:${
                part === 0 ? '00' : timestamp * part
              } ${PM_AM}`;

              return (
                <li
                  tabIndex={isSelected ? 0 : -1}
                  id={`time-${trueIndex}`}
                  key={`time-${trueIndex}`}
                  aria-label='currentTime'
                  className={cn(
                    buttonVariants({
                      variant: isSuggested ? 'secondary' : isSelected ? 'default' : 'outline-solid',
                    }),
                    'h-8 px-3 w-full text-sm focus-visible:outline-0 outline-0 focus-visible:border-0 cursor-default ring-0'
                  )}
                  onClick={() => handleClick(i, part, PM_AM, trueIndex)}
                  onFocus={() => isSuggested && setActiveIndex(trueIndex)}
                >
                  {currentValue}
                </li>
              );
            });
          })}
        </ul>
      </ScrollArea>
    </div>
  );
};
const getDefaultPlaceholder = (showCalendar: boolean, showTimePicker: boolean) => {
  if (!showCalendar && showTimePicker) {
    return 'e.g. "5pm" or "in 2 hours"';
  }
  if (showCalendar && !showTimePicker) {
    return 'e.g. "tomorrow" or "next monday"';
  }
  return 'e.g. "tomorrow at 5pm" or "in 2 hours"';
};
const NaturalLanguageInput = React.forwardRef<
  HTMLInputElement,
  {
    placeholder?: string;
    disabled?: boolean;
  }
>(({ placeholder, ...props }, ref) => {
  const { value, onValueChange, Time, onTimeChange, showCalendar, showTimePicker } =
    useSmartDateInput();

  const _placeholder = placeholder ?? getDefaultPlaceholder(showCalendar, showTimePicker);

  const [inputValue, setInputValue] = React.useState<string>('');

  React.useEffect(() => {
    if (!value) {
      setInputValue('');
      return;
    }

    const formattedValue = formatDateTime(value, showCalendar, showTimePicker);
    setInputValue(formattedValue);

    // Only update time if time picker is shown
    if (showTimePicker) {
      const hour = value.getHours();
      const timeVal = `${hour >= 12 ? hour % 12 || 12 : hour || 12}:${String(
        value.getMinutes()
      ).padStart(2, '0')} ${hour >= 12 ? 'PM' : 'AM'}`;
      onTimeChange(timeVal);
    }
  }, [value, showCalendar, showTimePicker]);

  const handleParse = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const parsedDateTime = parseDateTime(e.currentTarget.value);
      if (parsedDateTime) {
        // If only showing time picker, preserve the current date
        if (!showCalendar && showTimePicker && value) {
          parsedDateTime.setFullYear(value.getFullYear(), value.getMonth(), value.getDate());
        }
        // If only showing calendar, preserve the current time
        if (showCalendar && !showTimePicker && value) {
          parsedDateTime.setHours(0, 0, 0, 0);
        }
        // console.log(parsedDateTime);

        onValueChange(parsedDateTime);
        setInputValue(formatDateTime(parsedDateTime, showCalendar, showTimePicker));

        if (showTimePicker) {
          const PM_AM = parsedDateTime.getHours() >= 12 ? 'PM' : 'AM';
          const PM_AM_hour = parsedDateTime.getHours();
          const hour =
            PM_AM_hour > 12
              ? PM_AM_hour % 12
              : PM_AM_hour === 0 || PM_AM_hour === 12
                ? 12
                : PM_AM_hour;
          onTimeChange(`${hour}:${String(parsedDateTime.getMinutes()).padStart(2, '0')} ${PM_AM}`);
        }
      }
    },
    [value, showCalendar, showTimePicker]
  );

  const handleKeydown = React.useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter') {
        handleParse(e as any);
      }
    },
    [handleParse]
  );

  return (
    <input
      ref={ref}
      type='text'
      placeholder={_placeholder}
      value={inputValue}
      onChange={(e) => setInputValue(e.currentTarget.value)}
      onKeyDown={handleKeydown}
      onBlur={handleParse}
      className={cn(
        'px-2 mr-0.5 dark:bg-neutral-800 bg-neutral-50 flex-1 border-none h-8 rounded-sm',
        inputBase
      )}
      {...props}
    />
  );
});

NaturalLanguageInput.displayName = 'NaturalLanguageInput';

type DateTimeLocalInputProps = {} & CalendarProps;

const DateTimeLocalInput = ({ className, ...props }: DateTimeLocalInputProps) => {
  const { value, onValueChange, Time, showCalendar, showTimePicker } = useSmartDateInput();

  const formateSelectedDate = React.useCallback(
    (date: Date | undefined, selectedDate: Date, m: ActiveModifiers, e: React.MouseEvent) => {
      const parsedDateTime = new Date(selectedDate);

      if (!showTimePicker) {
        // If only calendar is shown, set time to start of day
        parsedDateTime.setHours(0, 0, 0, 0);
      } else if (value) {
        // If time picker is shown, preserve existing time
        parsedDateTime.setHours(
          value.getHours(),
          value.getMinutes(),
          value.getSeconds(),
          value.getMilliseconds()
        );
      }

      onValueChange(parsedDateTime);
    },
    [value, showTimePicker, onValueChange]
  );

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant={'outline'}
          size={'icon'}
          className={cn(
            'size-9 flex items-center justify-center font-normal dark:bg-neutral-800 bg-neutral-200',
            !value && 'text-muted-foreground'
          )}
        >
          <CalendarIcon className='size-4' />
          <span className='sr-only'>calendar</span>
        </Button>
      </PopoverTrigger>
      <PopoverContent className='w-auto p-0 dark:bg-neutral-800 bg-neutral-50' sideOffset={8}>
        <div className='flex gap-1'>
          {showCalendar && (
            <Calendar
              {...props}
              id={'calendar'}
              className={cn('peer flex justify-end', inputBase, className)}
              mode='single'
              selected={value}
              onSelect={formateSelectedDate}
              initialFocus
            />
          )}
          {showTimePicker && <TimePicker />}
        </div>
      </PopoverContent>
    </Popover>
  );
};

DateTimeLocalInput.displayName = 'DateTimeLocalInput';
./components/website/ui/button.tsx
import { Slot } from '@radix-ui/react-slot';
import { type VariantProps, cva } from 'class-variance-authority';
import * as React from 'react';

import { cn } from '@/lib/utils';

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline:
          'border border-input dark:bg-neutral-800 bg-neutral-50 hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
        uilayouts:
          'dark:bg-zinc-900 bg-neutral-200 dark:text-white text-black border dark:border-neutral-800',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : 'button';
    return (
      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
    );
  }
);
Button.displayName = 'Button';

export { Button, buttonVariants };
./components/website/ui/calendar.tsx
'use client';

import { buttonVariants } from '@/components/website/ui/button';
import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type * as React from 'react';
import { DayPicker } from 'react-day-picker';

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn('p-3', className)}
      classNames={{
        months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
        month: 'space-y-4',
        caption: 'flex justify-center pt-1 relative items-center',
        caption_label: 'text-sm font-medium',
        nav: 'space-x-1 flex items-center',
        nav_button: cn(
          buttonVariants({ variant: 'outline' }),
          'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
        ),
        nav_button_previous: 'absolute left-1',
        nav_button_next: 'absolute right-1',
        table: 'w-full border-collapse space-y-1',
        head_row: 'flex',
        head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
        row: 'flex w-full mt-2',
        cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
        day: cn(
          buttonVariants({ variant: 'ghost' }),
          'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
        ),
        day_range_end: 'day-range-end',
        day_selected:
          'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
        day_today: 'bg-accent text-accent-foreground',
        day_outside:
          'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
        day_disabled: 'text-muted-foreground opacity-50',
        day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
        day_hidden: 'invisible',
        ...classNames,
      }}
      components={{
        IconLeft: ({ ...props }) => <ChevronLeft className='h-4 w-4' />,
        IconRight: ({ ...props }) => <ChevronRight className='h-4 w-4' />,
      }}
      {...props}
    />
  );
}
Calendar.displayName = 'Calendar';

export { Calendar };
./components/website/ui/popover.tsx
'use client';

import * as PopoverPrimitive from '@radix-ui/react-popover';
import * as React from 'react';

import { cn } from '@/lib/utils';

const Popover = PopoverPrimitive.Root;

const PopoverTrigger = PopoverPrimitive.Trigger;

const PopoverContent = React.forwardRef<
  React.ElementRef<typeof PopoverPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
  <PopoverPrimitive.Portal>
    <PopoverPrimitive.Content
      ref={ref}
      align={align}
      sideOffset={sideOffset}
      className={cn(
        'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
        className
      )}
      {...props}
    />
  </PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;

export { Popover, PopoverContent, PopoverTrigger };
./components/website/ui/scroll-area.tsx
'use client';

import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import * as React from 'react';

import { cn } from '@/lib/utils';

const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn('relative overflow-hidden', className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className='h-full w-full rounded-[inherit]'>
      {children}
    </ScrollAreaPrimitive.Viewport>
    <ScrollBar />
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;

const ScrollBar = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
  <ScrollAreaPrimitive.ScrollAreaScrollbar
    ref={ref}
    orientation={orientation}
    className={cn(
      'flex touch-none select-none transition-colors',
      orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
      orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
      className
    )}
    {...props}
  >
    <ScrollAreaPrimitive.ScrollAreaThumb className='relative flex-1 rounded-full dark:bg-neutral-800 bg-neutral-200' />
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;

export { ScrollArea, ScrollBar };

Similar components