{
  "$schema": "https://abackui.com/schema/registry-item.json",
  "name": "date-picker",
  "type": "registry:ui",
  "title": "Date Picker",
  "description": "A date field and a range field, with real keyboard behavior and native form support.",
  "dependencies": [
    "@tabler/icons-react",
    "react-day-picker"
  ],
  "registryDependencies": [
    "https://abackui.com/r/utils.json",
    "https://abackui.com/r/button.json",
    "https://abackui.com/r/calendar.json",
    "https://abackui.com/r/popover.json"
  ],
  "categories": [
    "Forms"
  ],
  "docs": "https://abackui.com/docs/components/date-picker",
  "meta": {
    "status": "beta"
  },
  "files": [
    {
      "path": "components/ui/date-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconCalendar, IconX } from \"@tabler/icons-react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar, formatCalendarDate } from \"@/components/ui/calendar\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\n\nfunction toIsoDate(date: Date): string {\n  const year = date.getFullYear();\n  const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n  const day = `${date.getDate()}`.padStart(2, \"0\");\n  return `${year}-${month}-${day}`;\n}\n\nfunction selectionBounds(min?: Date, max?: Date) {\n  const matchers = [];\n  if (min) matchers.push({ before: min });\n  if (max) matchers.push({ after: max });\n  return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n  value?: Date;\n  onChange?: (date: Date | undefined) => void;\n  placeholder?: string;\n  /** Reject dates before this day. */\n  min?: Date;\n  /** Reject dates after this day. */\n  max?: Date;\n  disabled?: boolean;\n  /** Show the \"Clear\" action inside the calendar. Defaults to `true`. */\n  clearable?: boolean;\n  /** Locale used for the trigger label, for example `en-GB`. */\n  locale?: string;\n  /** Replace the default `Oct 24, 2026` label. */\n  format?: (date: Date) => string;\n  /** Month shown before the visitor picks a date. */\n  defaultMonth?: Date;\n  id?: string;\n  /** Renders a hidden input so the value submits with a native form. */\n  name?: string;\n  className?: string;\n  align?: \"start\" | \"center\" | \"end\";\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: boolean;\n  \"aria-labelledby\"?: string;\n}\n\n/**\n * A date field: a button that opens a calendar in a popover.\n *\n * The trigger is a real button, so the field is reachable and operable by\n * keyboard. Selecting a day closes the popover and returns focus to the\n * trigger; Escape closes without changing the value. When `name` is set, a\n * hidden input carries the `yyyy-mm-dd` value into native form submission.\n */\nfunction DatePicker({\n  value,\n  onChange,\n  placeholder = \"Pick a date\",\n  min,\n  max,\n  disabled,\n  clearable = true,\n  locale,\n  format,\n  defaultMonth,\n  id,\n  name,\n  className,\n  align = \"start\",\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n  \"aria-labelledby\": ariaLabelledBy,\n}: DatePickerProps) {\n  const [open, setOpen] = React.useState(false);\n  const label = value ? (format ? format(value) : formatCalendarDate(value, locale)) : placeholder;\n\n  function select(date: Date | undefined) {\n    onChange?.(date);\n    if (date) setOpen(false);\n  }\n\n  return (\n    <div data-slot=\"date-picker\" className={cn(\"relative w-full\", className)}>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            id={id}\n            type=\"button\"\n            variant=\"outline\"\n            disabled={disabled}\n            aria-haspopup=\"dialog\"\n            aria-expanded={open}\n            aria-describedby={ariaDescribedBy}\n            aria-invalid={ariaInvalid}\n            aria-labelledby={ariaLabelledBy}\n            className={cn(\n              \"w-full justify-start gap-2 font-normal\",\n              !value && \"text-muted-foreground\",\n            )}\n          >\n            <IconCalendar aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            <span className=\"truncate\">{label}</span>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent align={align} aria-label=\"Choose a date\" className=\"w-auto p-0\">\n          <Calendar\n            mode=\"single\"\n            selected={value}\n            onSelect={select}\n            defaultMonth={value ?? defaultMonth}\n            disabled={selectionBounds(min, max)}\n            autoFocus\n          />\n          {clearable && value ? (\n            <div className=\"border-t border-border p-1\">\n              <button\n                type=\"button\"\n                className=\"flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n                onClick={() => {\n                  onChange?.(undefined);\n                  setOpen(false);\n                }}\n              >\n                <IconX aria-hidden=\"true\" className=\"size-3.5\" />\n                Clear date\n              </button>\n            </div>\n          ) : null}\n        </PopoverContent>\n      </Popover>\n      {name ? <input type=\"hidden\" name={name} value={value ? toIsoDate(value) : \"\"} /> : null}\n    </div>\n  );\n}\n\nexport interface DateRangePickerProps {\n  value?: DateRange;\n  onChange?: (range: DateRange | undefined) => void;\n  placeholder?: string;\n  min?: Date;\n  max?: Date;\n  disabled?: boolean;\n  clearable?: boolean;\n  locale?: string;\n  format?: (date: Date) => string;\n  /** Number of months shown side by side on wide screens. Defaults to `1`. */\n  numberOfMonths?: number;\n  defaultMonth?: Date;\n  id?: string;\n  className?: string;\n  align?: \"start\" | \"center\" | \"end\";\n  \"aria-describedby\"?: string;\n  \"aria-labelledby\"?: string;\n}\n\n/**\n * A range field for reports, filters, and booking windows.\n *\n * The popover stays open until both ends are chosen, so the visitor can see\n * the in-progress range. A completed range is announced through the calendar\n * grid's `aria-multiselectable` and per-day selected state.\n */\nfunction DateRangePicker({\n  value,\n  onChange,\n  placeholder = \"Pick a date range\",\n  min,\n  max,\n  disabled,\n  clearable = true,\n  locale,\n  format,\n  numberOfMonths = 1,\n  defaultMonth,\n  id,\n  className,\n  align = \"start\",\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-labelledby\": ariaLabelledBy,\n}: DateRangePickerProps) {\n  const [open, setOpen] = React.useState(false);\n  const formatOne = (date: Date) =>\n    format ? format(date) : formatCalendarDate(date, locale);\n\n  const sameDay =\n    value?.from && value.to ? value.from.toDateString() === value.to.toDateString() : false;\n  const label = value?.from\n    ? value.to && !sameDay\n      ? `${formatOne(value.from)} – ${formatOne(value.to)}`\n      : formatOne(value.from)\n    : placeholder;\n\n  function select(range: DateRange | undefined) {\n    onChange?.(range);\n    // A first click in react-day-picker produces a one-day range. Keeping the\n    // calendar open lets the visitor finish the second end; a range that spans\n    // two different days is complete and closes the popover.\n    if (range?.from && range.to && range.from.toDateString() !== range.to.toDateString()) {\n      setOpen(false);\n    }\n  }\n\n  return (\n    <div data-slot=\"date-range-picker\" className={cn(\"relative w-full\", className)}>\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            id={id}\n            type=\"button\"\n            variant=\"outline\"\n            disabled={disabled}\n            aria-haspopup=\"dialog\"\n            aria-expanded={open}\n            aria-describedby={ariaDescribedBy}\n            aria-labelledby={ariaLabelledBy}\n            className={cn(\n              \"w-full justify-start gap-2 font-normal\",\n              !value?.from && \"text-muted-foreground\",\n            )}\n          >\n            <IconCalendar aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            <span className=\"truncate\">{label}</span>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent align={align} aria-label=\"Choose a date range\" className=\"w-auto p-0\">\n          <Calendar\n            mode=\"range\"\n            selected={value}\n            onSelect={select}\n            defaultMonth={value?.from ?? defaultMonth}\n            numberOfMonths={numberOfMonths}\n            disabled={selectionBounds(min, max)}\n            autoFocus\n          />\n          {clearable && value?.from ? (\n            <div className=\"border-t border-border p-1\">\n              <button\n                type=\"button\"\n                className=\"flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n                onClick={() => {\n                  onChange?.(undefined);\n                  setOpen(false);\n                }}\n              >\n                <IconX aria-hidden=\"true\" className=\"size-3.5\" />\n                Clear range\n              </button>\n            </div>\n          ) : null}\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\nexport { DatePicker, DateRangePicker };\n",
      "type": "registry:ui",
      "target": "components/ui/date-picker.tsx"
    }
  ]
}
