{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "smart-datetime-input",
  "title": "Smart Datetime Input",
  "description": "Natural-language date input (\"tomorrow at 3pm\") with a calendar and time list fallback.",
  "dependencies": [
    "chrono-node",
    "date-fns",
    "lucide-react",
    "react-day-picker"
  ],
  "registryDependencies": [
    "button",
    "calendar",
    "input",
    "popover",
    "scroll-area"
  ],
  "files": [
    {
      "path": "components/ui/smart-datetime-input.tsx",
      "content": "'use client'\n\nimport React from 'react'\nimport * as chrono from 'chrono-node'\nimport { enUS as localeEnUS, type Locale } from 'date-fns/locale'\nimport { Calendar as CalendarIcon } from 'lucide-react'\nimport type { Modifiers } from 'react-day-picker'\n\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Calendar } from '@/components/ui/calendar'\nimport { Input } from '@/components/ui/input'\nimport { buttonVariants } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { cn } from '@/lib/utils'\n\n/* -------------------------------------------------------------------------- */\n/*                               Inspired By:                                 */\n/*                               @steventey                                   */\n/* ------------------https://dub.co/blog/smart-datetime-picker--------------- */\n/* -------------------------------------------------------------------------- */\n\nconst localesUsings12HourTime = [\n  'en-US',\n  'en-CA',\n  'en-AU',\n  'en-NZ',\n  'en-PH',\n  'en-IN',\n  'en-IE',\n  'en-GB',\n  'en-ZA',\n  'en-NG',\n  'en-MY',\n  'en-BD',\n  'en-PK',\n  'es-US',\n  'es-MX',\n  'es-CO',\n  'es-PH',\n  'ar-SA',\n  'ar-EG',\n]\n\nconst chronoLocales = [\n  'en',\n  'de',\n  'fr',\n  'ja',\n  'pt',\n  'nl',\n  'zh',\n  'ru',\n  'es',\n  'uk',\n] as const\ntype ChronoLocale = (typeof chronoLocales)[number]\n\n/**\n * Utility function that parses dates.\n * Parses a given date string using the `chrono-node` library.\n *\n * @param str - A string representation of a date and time.\n * @param locale - The locale to use for parsing the date and time.\n * @returns A `Date` object representing the parsed date and time, or `null` if the string could not be parsed.\n */\nexport const parseDateTime = (str: Date | string, locale: Locale) => {\n  if (str instanceof Date) return str\n\n  const localeCode = chronoLocales.includes(locale.code as ChronoLocale)\n    ? (locale.code as ChronoLocale)\n    : chronoLocales.includes(locale.code.split('-')[0] as ChronoLocale)\n      ? (locale.code.split('-')[0] as ChronoLocale)\n      : 'en'\n\n  return chrono[localeCode].parseDate(str)\n}\n\n/**\n * Converts a given timestamp or the current date and time to a string representation in the local time zone.\n * format: `HH:mm`, adjusted for the local time zone.\n *\n * @param timestamp {Date | string}\n * @returns A string representation of the timestamp\n */\nexport const getDateTimeLocal = (timestamp?: Date): string => {\n  const d = timestamp ? new Date(timestamp) : new Date()\n  if (d.toString() === 'Invalid Date') return ''\n\n  return new Date(d.getTime() - d.getTimezoneOffset() * 60000)\n    .toISOString()\n    .split(':')\n    .slice(0, 2)\n    .join(':')\n}\n\n/**\n * Formats a given date and time object or string into a human-readable string representation.\n * \"MMM D, YYYY h:mm A\" (e.g. \"Jan 1, 2023 12:00 PM\").\n *\n * @param datetime - {Date | string}\n * @param hour12 - Whether to use 12-hour time (true) or 24-hour time (false).\n * @param locale - The locale to use for formatting the date and time.\n * @returns A string representation of the date and time\n */\nexport const formatDateTime = (\n  datetime: Date | string,\n  locale: Locale,\n  hour12: boolean,\n) =>\n  new Date(datetime).toLocaleTimeString(locale.code, {\n    month: 'short',\n    day: 'numeric',\n    year: 'numeric',\n    hour: 'numeric',\n    minute: 'numeric',\n    hour12,\n  })\n\nconst inputBase =\n  'bg-transparent focus:outline-none focus:ring-0 focus-within:outline-none focus-within:ring-0 sm:text-sm disabled:cursor-not-allowed disabled:opacity-50'\n\n// @source: https://www.perplexity.ai/search/in-javascript-how-RfI7fMtITxKr5c.V9Lv5KA#1\n// use this pattern to validate the transformed date string for the natural language input\nconst naturalInputValidationPattern =\n  '^[A-Z][a-z]{2}sd{1,2},sd{4},sd{1,2}:d{2}s[AP]M$'\n\nconst DEFAULT_SIZE = 96\n\n/**\n * Smart time input Docs: {@link: https://shadcn-extension.vercel.app/docs/smart-time-input}\n */\ntype SmartDatetimeInputProps = Omit<\n  React.ComponentProps<typeof Calendar>,\n  'id' | 'mode' | 'selected' | 'onSelect' | 'locale'\n> & {\n  locale?: Locale\n  value?: Date | null\n  onValueChange: (date: Date | null) => void\n  hour12?: boolean\n}\n\ntype SmartDatetimeInputContextProps = SmartDatetimeInputProps & {\n  Time: string\n  onTimeChange: (time: string) => void\n}\n\nconst SmartDatetimeInputContext =\n  React.createContext<SmartDatetimeInputContextProps | null>(null)\n\nconst useSmartDateInput = () => {\n  const context = React.useContext(SmartDatetimeInputContext)\n  if (!context) {\n    throw new Error(\n      'useSmartDateInput must be used within SmartDateInputProvider',\n    )\n  }\n  return context\n}\n\nexport const SmartDatetimeInput = React.forwardRef<\n  HTMLInputElement,\n  Omit<\n    React.InputHTMLAttributes<HTMLInputElement>,\n    'type' | 'ref' | 'value' | 'defaultValue' | 'onBlur' | 'disabled'\n  > &\n    SmartDatetimeInputProps\n>(\n  (\n    {\n      className,\n      value,\n      locale = localeEnUS,\n      hour12 = localesUsings12HourTime.includes(locale.code),\n      onValueChange,\n      placeholder,\n      disabled,\n      ...dateTimeInputProps\n    },\n    ref,\n  ) => {\n    // ? refactor to be only used with controlled input\n    /*  const [dateTime, setDateTime] = React.useState<Date | undefined>(\n    value ?? undefined\n  ); */\n\n    const [Time, setTime] = React.useState<string>('')\n\n    const onTimeChange = React.useCallback((time: string) => {\n      setTime(time)\n    }, [])\n\n    return (\n      <SmartDatetimeInputContext.Provider\n        value={{ value, onValueChange, Time, onTimeChange }}\n      >\n        <div className=\"flex items-center justify-center\">\n          <div\n            className={cn(\n              'flex gap-1 w-full p-1 items-center justify-between rounded-md border transition-all',\n              'focus-within:outline-0 focus:outline-0 focus:ring-0',\n              'placeholder:text-muted-foreground focus-visible:outline-0 ',\n              className,\n            )}\n          >\n            <DateTimeLocalInput\n              locale={locale}\n              hour12={hour12}\n              disabled={disabled}\n              {...dateTimeInputProps}\n            />\n            <NaturalLanguageInput\n              ref={ref}\n              locale={locale}\n              hour12={hour12}\n              placeholder={placeholder}\n              disabled={typeof disabled === 'boolean' ? disabled : undefined}\n            />\n          </div>\n        </div>\n      </SmartDatetimeInputContext.Provider>\n    )\n  },\n)\n\nSmartDatetimeInput.displayName = 'DatetimeInput'\n\n// Make it a standalone component\ntype TimePickerProps = {\n  locale: Locale\n  hour12: boolean\n}\n\nconst TimePicker = ({ hour12, locale }: TimePickerProps) => {\n  const { value, onValueChange, Time, onTimeChange } = useSmartDateInput()\n  const [activeIndex, setActiveIndex] = React.useState(-1)\n  const timestamp = 15\n\n  const formateSelectedTime = React.useCallback(\n    (time: string, hour: number, partStamp: number) => {\n      onTimeChange(time)\n\n      const newVal = parseDateTime(value ?? new Date(), locale)\n      if (!newVal) return\n\n      newVal.setHours(\n        hour,\n        partStamp === 0 ? parseInt('00') : timestamp * partStamp,\n      )\n\n      // ? refactor needed check if we want to use the new date\n\n      onValueChange(newVal)\n    },\n    [locale, value, onValueChange, onTimeChange],\n  )\n\n  const handleKeydown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      e.stopPropagation()\n\n      if (!document) return\n\n      const moveNext = () => {\n        const nextIndex =\n          activeIndex + 1 > DEFAULT_SIZE - 1 ? 0 : activeIndex + 1\n\n        const currentElm = document.getElementById(`time-${nextIndex}`)\n\n        currentElm?.focus()\n\n        setActiveIndex(nextIndex)\n      }\n\n      const movePrev = () => {\n        const prevIndex =\n          activeIndex - 1 < 0 ? DEFAULT_SIZE - 1 : activeIndex - 1\n\n        const currentElm = document.getElementById(`time-${prevIndex}`)\n\n        currentElm?.focus()\n\n        setActiveIndex(prevIndex)\n      }\n\n      const setElement = () => {\n        const currentElm = document.getElementById(`time-${activeIndex}`)\n\n        if (!currentElm) return\n\n        currentElm.focus()\n\n        const timeValue = currentElm.textContent ?? ''\n\n        let hour = parseInt(timeValue.split(':')[0])\n        const minutes = parseInt(timeValue.split(':')[1].split(' ')[0])\n\n        if (hour12) {\n          const PM_AM = timeValue.split(' ')[1]\n          hour =\n            PM_AM === 'AM'\n              ? hour === 12\n                ? 0\n                : hour\n              : hour === 12\n                ? 12\n                : hour + 12\n        }\n\n        const part = Math.floor(minutes / 15)\n\n        formateSelectedTime(timeValue, hour, part)\n      }\n\n      const reset = () => {\n        const currentElm = document.getElementById(`time-${activeIndex}`)\n        currentElm?.blur()\n        setActiveIndex(-1)\n      }\n\n      switch (e.key) {\n        case 'ArrowUp':\n          movePrev()\n          break\n\n        case 'ArrowDown':\n          moveNext()\n          break\n\n        case 'Escape':\n          reset()\n          break\n\n        case 'Enter':\n          setElement()\n          break\n      }\n    },\n    [activeIndex, formateSelectedTime, hour12],\n  )\n\n  const handleClick = React.useCallback(\n    (hour: number, part: number, PM_AM: string, currentIndex: number) => {\n      let displayHour = hour\n      if (hour12 && hour > 12) {\n        displayHour = hour % 12\n      }\n      const formattedHour = displayHour === 0 ? 12 : displayHour\n      const formattedTime = hour12\n        ? `${formattedHour}:${part === 0 ? '00' : timestamp * part} ${PM_AM}`\n        : `${hour}:${part === 0 ? '00' : timestamp * part}`\n      formateSelectedTime(formattedTime, hour, part)\n      setActiveIndex(currentIndex)\n    },\n    [formateSelectedTime, hour12],\n  )\n\n  const currentTime = React.useMemo(() => {\n    const timeVal = Time.split(' ')[0]\n    return {\n      hours: parseInt(timeVal.split(':')[0]),\n      minutes: parseInt(timeVal.split(':')[1]),\n    }\n  }, [Time])\n\n  React.useEffect(() => {\n    const getCurrentElementTime = () => {\n      const timeVal = Time.split(' ')[0]\n      const hours = parseInt(timeVal.split(':')[0])\n      const minutes = parseInt(timeVal.split(':')[1])\n      const PM_AM = hour12 ? Time.split(' ')[1] : ''\n\n      const formatIndex = !hour12\n        ? hours\n        : PM_AM === 'AM'\n          ? hours\n          : hours === 12\n            ? hours\n            : hours + 12\n      const formattedHours = formatIndex\n\n      for (let j = 0; j <= 3; j++) {\n        const diff = Math.abs(j * timestamp - minutes)\n        const selected =\n          !hour12 ||\n          (PM_AM === (formattedHours >= 12 ? 'PM' : 'AM') &&\n            (minutes <= 53\n              ? diff < Math.ceil(timestamp / 2)\n              : diff < timestamp))\n\n        if (selected) {\n          const trueIndex =\n            activeIndex === -1 ? formattedHours * 4 + j : activeIndex\n\n          setActiveIndex(trueIndex)\n\n          const currentElm = document.getElementById(`time-${trueIndex}`)\n          currentElm?.scrollIntoView({\n            block: 'center',\n            behavior: 'smooth',\n          })\n        }\n      }\n    }\n\n    getCurrentElementTime()\n  }, [Time, activeIndex, hour12])\n\n  const height = React.useMemo(() => {\n    if (!document) return\n    const calendarElm = document.getElementById('calendar')\n    if (!calendarElm) return\n    return calendarElm.style.height\n  }, [])\n\n  return (\n    <div className=\"space-y-2 pr-3 py-3 relative \">\n      <ScrollArea\n        onKeyDown={handleKeydown}\n        className=\"h-full w-full focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-0 py-0.5\"\n        style={{\n          height,\n        }}\n      >\n        <ul\n          className={cn(\n            'flex items-center flex-col gap-1 h-full max-h-56 w-28 px-1 py-0.5',\n          )}\n        >\n          {Array.from({ length: 24 }).map((_, i) => {\n            const PM_AM = i >= 12 ? 'PM' : 'AM'\n            const formatIndex = i > 12 ? i % 12 : i === 0 || i === 12 ? 12 : i\n\n            return Array.from({ length: 4 }).map((_, part) => {\n              const diff = Math.abs(part * timestamp - currentTime.minutes)\n\n              const trueIndex = i * 4 + part\n\n              const isSelected =\n                (currentTime.hours === i ||\n                  currentTime.hours === formatIndex) &&\n                (!hour12 || Time.split(' ')[1] === PM_AM) &&\n                (currentTime.minutes <= 53\n                  ? diff < Math.ceil(timestamp / 2)\n                  : diff < timestamp)\n\n              const isSuggested = !value && isSelected\n\n              const currentValue = !hour12\n                ? `${i}:${part === 0 ? '00' : timestamp * part}`\n                : `${formatIndex}:${\n                    part === 0 ? '00' : timestamp * part\n                  } ${PM_AM}`\n\n              return (\n                <li\n                  tabIndex={isSelected ? 0 : -1}\n                  id={`time-${trueIndex}`}\n                  key={`time-${trueIndex}`}\n                  aria-label=\"currentTime\"\n                  className={cn(\n                    buttonVariants({\n                      variant: isSuggested\n                        ? 'secondary'\n                        : isSelected\n                          ? 'default'\n                          : 'outline',\n                    }),\n                    'h-8 px-3 w-full text-sm focus-visible:outline-0 outline-0 focus-visible:border-0 cursor-default ring-0',\n                  )}\n                  onClick={() => handleClick(i, part, PM_AM, trueIndex)}\n                  onFocus={() => isSuggested && setActiveIndex(trueIndex)}\n                >\n                  {currentValue}\n                </li>\n              )\n            })\n          })}\n        </ul>\n      </ScrollArea>\n    </div>\n  )\n}\n\nconst NaturalLanguageInput = React.forwardRef<\n  HTMLInputElement,\n  {\n    locale: Locale\n    hour12: boolean\n    placeholder?: string\n    disabled?: boolean\n  }\n>(({ locale, hour12, placeholder, ...props }, ref) => {\n  const { value, onValueChange, Time, onTimeChange } = useSmartDateInput()\n\n  const _placeholder =\n    placeholder ??\n    (locale.code.split('-')[0] === 'fr'\n      ? 'e.g. \"demain à 17h\" ou \"dans 2 heures\"'\n      : 'e.g. \"tomorrow at 5pm\" or \"in 2 hours\"')\n\n  const [inputValue, setInputValue] = React.useState<string>('')\n\n  React.useEffect(() => {\n    const hour = new Date().getHours()\n    const timeVal = hour12\n      ? `${hour >= 12 ? hour % 12 : hour}:${new Date().getMinutes()} ${\n          hour >= 12 ? 'PM' : 'AM'\n        }`\n      : `${hour}:${new Date().getMinutes()}`\n\n    setInputValue(value ? formatDateTime(value, locale, hour12) : '')\n    onTimeChange(value ? Time : timeVal)\n  }, [hour12, locale, value, Time, onTimeChange])\n\n  const handleParse = React.useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      if (e.currentTarget.value === '') {\n        onTimeChange('')\n        onValueChange(null)\n        return\n      }\n\n      // parse the date string when the input field loses focus\n      const parsedDateTime = parseDateTime(e.currentTarget.value, locale)\n      if (parsedDateTime) {\n        if (hour12) {\n          const PM_AM = parsedDateTime.getHours() >= 12 ? 'PM' : 'AM'\n          //fix the time format for this value\n\n          const PM_AM_hour = parsedDateTime.getHours()\n\n          const hour =\n            PM_AM_hour > 12\n              ? PM_AM_hour % 12\n              : PM_AM_hour === 0 || PM_AM_hour === 12\n                ? 12\n                : PM_AM_hour\n\n          onValueChange(parsedDateTime)\n          setInputValue(formatDateTime(parsedDateTime, locale, hour12))\n          onTimeChange(`${hour}:${parsedDateTime.getMinutes()} ${PM_AM}`)\n        } else {\n          onValueChange(parsedDateTime)\n          setInputValue(formatDateTime(parsedDateTime, locale, hour12))\n          onTimeChange(\n            `${parsedDateTime.getHours()}:${parsedDateTime.getMinutes()}`,\n          )\n        }\n      }\n    },\n    [locale, hour12, onValueChange, onTimeChange],\n  )\n\n  const handleKeydown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      switch (e.key) {\n        case 'Enter':\n          const parsedDateTime = parseDateTime(e.currentTarget.value, locale)\n          if (parsedDateTime) {\n            const PM_AM = parsedDateTime.getHours() >= 12 ? 'PM' : 'AM'\n            //fix the time format for this value\n\n            const PM_AM_hour = parsedDateTime.getHours()\n\n            const hour =\n              PM_AM_hour > 12\n                ? PM_AM_hour % 12\n                : PM_AM_hour === 0 || PM_AM_hour === 12\n                  ? 12\n                  : PM_AM_hour\n\n            onValueChange(parsedDateTime)\n            setInputValue(formatDateTime(parsedDateTime, locale, hour12))\n            onTimeChange(`${hour}:${parsedDateTime.getMinutes()} ${PM_AM}`)\n          }\n          break\n      }\n    },\n    [locale, hour12, onValueChange, onTimeChange],\n  )\n\n  return (\n    <Input\n      ref={ref}\n      type=\"text\"\n      placeholder={_placeholder}\n      value={inputValue}\n      onChange={(e) => {\n        setInputValue(e.target.value)\n      }}\n      onKeyDown={handleKeydown}\n      onBlur={handleParse}\n      className={cn('px-2 mr-0.5 flex-1 border-none h-8 rounded', inputBase)}\n      {...props}\n    />\n  )\n})\n\nNaturalLanguageInput.displayName = 'NaturalLanguageInput'\n\ntype DateTimeLocalInputProps = {\n  locale: Locale\n  hour12: boolean\n} & Omit<React.ComponentProps<typeof Calendar>, 'locale'>\n\nconst DateTimeLocalInput = ({\n  locale,\n  hour12,\n  className,\n  ...props\n}: DateTimeLocalInputProps) => {\n  const { value, onValueChange, Time } = useSmartDateInput()\n\n  const formateSelectedDate = React.useCallback(\n    (\n      date: Date | undefined,\n      selectedDate: Date,\n      _m: Modifiers,\n      _e: React.MouseEvent | React.KeyboardEvent,\n    ) => {\n      const parsedDateTime = parseDateTime(selectedDate, locale)\n\n      if (parsedDateTime) {\n        parsedDateTime.setHours(\n          parseInt(Time.split(':')[0]),\n          parseInt(Time.split(':')[1]),\n        )\n        onValueChange(parsedDateTime)\n      }\n    },\n    [locale, Time, onValueChange],\n  )\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        type=\"button\"\n        className={cn(\n          buttonVariants({ variant: 'outline', size: 'icon' }),\n          'size-9 flex items-center justify-center font-normal',\n          !value && 'text-muted-foreground',\n        )}\n      >\n        <CalendarIcon className=\"size-4\" />\n        <span className=\"sr-only\">Open calendar</span>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-auto p-0\" sideOffset={8}>\n        <div className=\"flex gap-1\">\n          <Calendar\n            {...props}\n            locale={locale}\n            id={'calendar'}\n            className={cn('peer flex justify-end', inputBase, className)}\n            mode=\"single\"\n            selected={value ?? undefined}\n            onSelect={formateSelectedDate}\n            autoFocus\n          />\n          <TimePicker locale={locale} hour12={hour12} />\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nDateTimeLocalInput.displayName = 'DateTimeLocalInput'\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "input",
    "date-time"
  ],
  "type": "registry:ui"
}