{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "datetime-picker",
  "title": "Date Time Picker",
  "description": "Combined date and time picker in a popover.",
  "dependencies": [
    "date-fns@^4.4.0",
    "lucide-react@^1.48.0",
    "react-day-picker@^10.0.1"
  ],
  "registryDependencies": [
    "https://rdf.dev.360online.com/r/button.json",
    "https://rdf.dev.360online.com/r/calendar.json",
    "https://rdf.dev.360online.com/r/popover.json",
    "https://rdf.dev.360online.com/r/scroll-area.json"
  ],
  "files": [
    {
      "path": "registry/rapid/ui/datetime-picker/datetime-picker.tsx",
      "content": "'use client'\n\nimport * as DateFns from 'date-fns'\nimport { CalendarIcon } from 'lucide-react'\nimport * as React from 'react'\nimport type { Matcher } from 'react-day-picker'\nimport { Button } from '@/components/ui/button'\nimport { Calendar } from '@/components/ui/calendar'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'\nimport { cn } from '@/lib/utils'\n\ninterface DateTimePickerProps {\n\treadonly value?: string\n\treadonly onChange?: (value: string) => void\n\treadonly placeholder?: string\n\t/** when true, the trigger button is disabled */\n\treadonly disabled?: boolean\n\t/** enable time selection UI in the popover */\n\treadonly enableTime?: boolean\n\t/** 12 or 24 hour cycle for time selection */\n\treadonly hourCycle?: 12 | 24\n\t/** locale for displaying the date (e.g. 'fi', 'sv', 'en') */\n\treadonly locale?: string\n\t/** when true, component is read-only (no popover interaction) */\n\treadonly readOnly?: boolean\n\t/** earliest month visible in the calendar / year dropdown */\n\treadonly startMonth?: Date\n\t/** latest month visible in the calendar / year dropdown */\n\treadonly endMonth?: Date\n\t/** dates that cannot be selected (react-day-picker matcher) */\n\treadonly disabledDates?: Matcher | Matcher[]\n\t/** extra classes for the trigger button */\n\treadonly className?: string\n\t/** id for the trigger button (e.g. to pair with a `<Label htmlFor>`) */\n\treadonly id?: string\n\t/** marks the trigger as invalid (destructive border + ring) */\n\treadonly 'aria-invalid'?: React.AriaAttributes['aria-invalid']\n\t/** accessible name when there is no visible `<Label htmlFor={id}>` */\n\treadonly 'aria-label'?: string\n\t/**\n\t * Ids of the elements that name the picker, usually the field's label. The button adds\n\t * itself, so the name ends with the chosen value (or the placeholder): \"Date of birth,\n\t * required, 15 Oct 2026\". Takes precedence over `aria-label`.\n\t */\n\treadonly 'aria-labelledby'?: string\n\t/** Ids of the help text and error message, passed on to the button */\n\treadonly 'aria-describedby'?: string\n}\n\n/**\n * Format a Date to a string value for form submission\n * - Without time: YYYY-MM-DD\n * - With time: YYYY-MM-DDTHH:mm (ISO 8601)\n */\nfunction formatDateValue(date: Date | undefined, enableTime: boolean): string {\n\tif (!date) return ''\n\treturn DateFns.format(date, enableTime ? \"yyyy-MM-dd'T'HH:mm\" : 'yyyy-MM-dd')\n}\n\nfunction formatDisplay(date: Date, locale: string, enableTime: boolean): string {\n\tconst options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }\n\tif (enableTime) {\n\t\toptions.hour = '2-digit'\n\t\toptions.minute = '2-digit'\n\t}\n\treturn date.toLocaleDateString(locale, options)\n}\n\nexport function DateTimePicker({\n\tvalue,\n\tonChange,\n\tplaceholder,\n\tdisabled,\n\tenableTime = false,\n\thourCycle = 24,\n\tlocale = 'en-GB',\n\treadOnly,\n\tstartMonth,\n\tendMonth,\n\tdisabledDates,\n\tclassName,\n\tid,\n\t'aria-invalid': ariaInvalid,\n\t'aria-label': ariaLabel,\n\t'aria-labelledby': ariaLabelledby,\n\t'aria-describedby': ariaDescribedby,\n}: DateTimePickerProps) {\n\tconst [open, setOpen] = React.useState(false)\n\tconst [date, setDate] = React.useState<Date | undefined>(\n\t\tvalue ? (value.includes('T') ? new Date(value) : new Date(`${value}T00:00:00`)) : undefined,\n\t)\n\tconst generatedId = React.useId()\n\tconst buttonId = id ?? generatedId\n\n\t// keep internal state in sync with external value\n\tReact.useEffect(() => {\n\t\tif (!value) {\n\t\t\tsetDate(undefined)\n\t\t\treturn\n\t\t}\n\t\tconst next = value.includes('T') ? new Date(value) : new Date(`${value}T00:00:00`)\n\t\tif (!Number.isNaN(next.getTime())) setDate(next)\n\t}, [value])\n\n\tconst hours = React.useMemo(\n\t\t() => (hourCycle === 12 ? Array.from({ length: 12 }, (_, i) => i + 1) : Array.from({ length: 24 }, (_, i) => i)),\n\t\t[hourCycle],\n\t)\n\n\tconst buttonLabel = date ? formatDisplay(date, locale, enableTime) : placeholder\n\n\tfunction handleHourChange(newDate: Date, hourValue: string) {\n\t\tif (hourCycle === 12) {\n\t\t\tconst hour = Number.parseInt(hourValue, 10)\n\t\t\tconst currentHours = newDate.getHours()\n\t\t\tconst isPM = currentHours >= 12\n\t\t\tlet newHour = hour === 12 ? 0 : hour\n\t\t\tif (isPM) newHour += 12\n\t\t\tnewDate.setHours(newHour)\n\t\t} else {\n\t\t\tnewDate.setHours(Number.parseInt(hourValue, 10))\n\t\t}\n\t}\n\n\tfunction handleMinuteChange(newDate: Date, minuteValue: string) {\n\t\tnewDate.setMinutes(Number.parseInt(minuteValue, 10))\n\t}\n\n\tfunction handleAmPmChange(newDate: Date, ampmValue: string) {\n\t\tconst currentHours = newDate.getHours()\n\t\tconst isPM = ampmValue === 'PM'\n\t\tif (isPM && currentHours < 12) {\n\t\t\tnewDate.setHours(currentHours + 12)\n\t\t} else if (!isPM && currentHours >= 12) {\n\t\t\tnewDate.setHours(currentHours - 12)\n\t\t}\n\t}\n\n\tfunction handleTimeChange(type: 'hour' | 'minute' | 'ampm', nextValue: string) {\n\t\tif (!date) return\n\t\tconst newDate = new Date(date)\n\n\t\tswitch (type) {\n\t\t\tcase 'hour':\n\t\t\t\thandleHourChange(newDate, nextValue)\n\t\t\t\tbreak\n\t\t\tcase 'minute':\n\t\t\t\thandleMinuteChange(newDate, nextValue)\n\t\t\t\tbreak\n\t\t\tcase 'ampm':\n\t\t\t\thandleAmPmChange(newDate, nextValue)\n\t\t\t\tbreak\n\t\t}\n\n\t\tsetDate(newDate)\n\t\tonChange?.(formatDateValue(newDate, enableTime))\n\t}\n\n\treturn (\n\t\t<Popover open={open && !readOnly} onOpenChange={setOpen}>\n\t\t\t<PopoverTrigger\n\t\t\t\tdisabled={disabled}\n\t\t\t\trender={\n\t\t\t\t\t<Button\n\t\t\t\t\t\tdata-read-only={readOnly || undefined}\n\t\t\t\t\t\tdata-empty={!date || undefined}\n\t\t\t\t\t\taria-invalid={ariaInvalid}\n\t\t\t\t\t\taria-disabled={disabled}\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tid={buttonId}\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t'justify-between font-normal hover:border-primary hover:bg-transparent hover:text-foreground data-empty:text-muted-foreground data-read-only:pointer-events-none data-read-only:cursor-default data-read-only:border-dashed data-read-only:border-input data-read-only:bg-muted/50 data-read-only:text-foreground data-read-only:hover:border-input data-read-only:hover:bg-muted/50',\n\t\t\t\t\t\t\tclassName,\n\t\t\t\t\t\t)}\n\t\t\t\t\t\taria-label={ariaLabelledby ? undefined : ariaLabel}\n\t\t\t\t\t\taria-labelledby={ariaLabelledby ? `${ariaLabelledby} ${buttonId}` : undefined}\n\t\t\t\t\t\taria-describedby={ariaDescribedby}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t<span className=\"truncate\">{buttonLabel}</span>\n\t\t\t\t<CalendarIcon data-icon=\"inline-end\" className=\"text-muted-foreground\" />\n\t\t\t</PopoverTrigger>\n\t\t\t<PopoverContent\n\t\t\t\tclassName=\"max-h-(--available-height) w-auto overflow-y-auto p-0\"\n\t\t\t\talign=\"start\"\n\t\t\t\t// Named after the field, so the calendar is not announced as just \"dialog\"\n\t\t\t\taria-labelledby={ariaLabelledby}\n\t\t\t\taria-label={ariaLabelledby ? undefined : ariaLabel}\n\t\t\t>\n\t\t\t\t<div className=\"sm:flex\">\n\t\t\t\t\t<Calendar\n\t\t\t\t\t\tmode=\"single\"\n\t\t\t\t\t\tselected={date}\n\t\t\t\t\t\tcaptionLayout=\"dropdown\"\n\t\t\t\t\t\tstartMonth={startMonth}\n\t\t\t\t\t\tendMonth={endMonth}\n\t\t\t\t\t\tdisabled={disabledDates}\n\t\t\t\t\t\tonSelect={(nextDate) => {\n\t\t\t\t\t\t\tsetDate(nextDate)\n\t\t\t\t\t\t\tonChange?.(formatDateValue(nextDate, enableTime))\n\t\t\t\t\t\t\tif (!enableTime) setOpen(false)\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t{enableTime && (\n\t\t\t\t\t\t<div className=\"flex flex-col divide-y border-t sm:h-[300px] sm:flex-row sm:divide-x sm:divide-y-0 sm:border-t-0 sm:border-l\">\n\t\t\t\t\t\t\t<ScrollArea className=\"w-64 sm:w-auto\">\n\t\t\t\t\t\t\t\t<div className=\"flex sm:flex-col p-2\">\n\t\t\t\t\t\t\t\t\t{hours.map((hour) => {\n\t\t\t\t\t\t\t\t\t\tconst isSelected =\n\t\t\t\t\t\t\t\t\t\t\t!!date && (hourCycle === 12 ? (date.getHours() % 12 || 12) === hour : date.getHours() === hour)\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\tkey={hour}\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\tvariant={isSelected ? 'default' : 'ghost'}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"aspect-square shrink-0 sm:w-full\"\n\t\t\t\t\t\t\t\t\t\t\t\tdisabled={!date}\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => handleTimeChange('hour', hour.toString())}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{hourCycle === 24 ? hour.toString().padStart(2, '0') : hour}\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<ScrollBar orientation=\"horizontal\" className=\"sm:hidden\" />\n\t\t\t\t\t\t\t</ScrollArea>\n\t\t\t\t\t\t\t<ScrollArea className=\"w-64 sm:w-auto\">\n\t\t\t\t\t\t\t\t<div className=\"flex sm:flex-col p-2\">\n\t\t\t\t\t\t\t\t\t{Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tkey={minute}\n\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\tvariant={date && date.getMinutes() === minute ? 'default' : 'ghost'}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"aspect-square shrink-0 sm:w-full\"\n\t\t\t\t\t\t\t\t\t\t\tdisabled={!date}\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => handleTimeChange('minute', minute.toString())}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{minute.toString().padStart(2, '0')}\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<ScrollBar orientation=\"horizontal\" className=\"sm:hidden\" />\n\t\t\t\t\t\t\t</ScrollArea>\n\t\t\t\t\t\t\t{hourCycle === 12 && (\n\t\t\t\t\t\t\t\t<ScrollArea>\n\t\t\t\t\t\t\t\t\t<div className=\"flex sm:flex-col p-2\">\n\t\t\t\t\t\t\t\t\t\t{['AM', 'PM'].map((ampm) => (\n\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\tkey={ampm}\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\tvariant={\n\t\t\t\t\t\t\t\t\t\t\t\t\tdate && ((ampm === 'AM' && date.getHours() < 12) || (ampm === 'PM' && date.getHours() >= 12))\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? 'default'\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: 'ghost'\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"aspect-square shrink-0 sm:w-full\"\n\t\t\t\t\t\t\t\t\t\t\t\tdisabled={!date}\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => handleTimeChange('ampm', ampm)}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{ampm}\n\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</ScrollArea>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/datetime-picker.tsx"
    }
  ],
  "type": "registry:ui"
}