Todos os componentes

Date Picker

Date Picker é um componente de formulário para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

UI Layouts MIT forms

O que é

Date Picker é um componente de formulário para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

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

Licença

Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Date Picker

./registry/components/form/datetime-picker/date-picker.tsx
'use client';

import { SmartDatetimeInput } from '@/components/ui/datetime-input';
import React, { useState } from 'react';

export default function Component() {
  const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);

  const handleDateChange = (date: Date) => {
    setSelectedDate(date);
    console.log('Selected date:', date);
  };

  return (
    <div className='p-4 max-w-md mx-auto py-10'>
      <SmartDatetimeInput
        value={selectedDate}
        showCalendar={true}
        showTimePicker={false}
        onValueChange={handleDateChange}
        placeholder='Enter a date and time'
        className='lg:w-96'
      />
      {/* {selectedDate && (
        <p className='mt-4'>Date: {selectedDate.toLocaleString()}</p>
      )} */}
    </div>
  );
}
./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 };

Componentes parecidos