{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-picker",
  "title": "Date Picker",
  "author": "@fma-ui/ui",
  "description": "A typed date input paired with a calendar popover over one value, for single dates (`DatePicker`) or ranges (`DateRangePicker`). Supports month- and year-only granularity, `unavailableRanges`, and a non-modal popover: the input stays editable while the calendar is open, and the calendar follows what is typed.",
  "dependencies": [
    "@internationalized/date@^3.12.4",
    "cn@^0.4.0",
    "lucide-react@^1.52.0",
    "react-aria-components@^1.21.1"
  ],
  "registryDependencies": [
    "@fma-ui/calendar",
    "@fma-ui/date-field",
    "@fma-ui/input-group",
    "@fma-ui/popover",
    "@fma-ui/date-granularity",
    "@fma-ui/types"
  ],
  "files": [
    {
      "path": "src/core/date-picker/date-picker.tsx",
      "content": "\"use client\";\n\nimport type { DateValue } from \"@internationalized/date\";\nimport { cn } from \"cn\";\nimport { CalendarIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  DatePicker as AriaDatePicker,\n  type DatePickerProps as AriaDatePickerProps,\n  DateRangePicker as AriaDateRangePicker,\n  type DateRangePickerProps as AriaDateRangePickerProps,\n  DialogContext,\n  OverlayTriggerStateContext,\n  type RangeValue,\n} from \"react-aria-components\";\nimport { Calendar, RangeCalendar } from \"@/core/calendar/calendar\";\nimport {\n  type DateConstraintProps,\n  DateInput,\n  useBlockedValueCorrection,\n  useControlledValue,\n  useDateConstraints,\n  useFocusWithin,\n} from \"@/core/date-field/date-field\";\nimport { InputGroup, InputGroupAddon, InputGroupButton } from \"@/core/input-group/input-group\";\nimport { Popover } from \"@/core/popover/popover\";\nimport { type DateGranularity, periodEnd, periodStart } from \"@/lib/date-granularity\";\nimport type { Interval } from \"@/lib/types\";\n\ninterface DatePickerContextValue {\n  isRange: boolean;\n  period: DateGranularity;\n  minValue?: DateValue | null;\n  maxValue?: DateValue | null;\n  // The consumer's own predicate, not the period-lifted one: the calendar\n  // lifts it to periods itself and combines it with `unavailableRanges`.\n  isDateUnavailable?: (date: DateValue) => boolean;\n  unavailableRanges?: Interval<DateValue>[];\n  rootRef: React.RefObject<HTMLDivElement | null>;\n}\n\nconst DatePickerContext = React.createContext<DatePickerContextValue | null>(null);\n\nfunction useDatePickerContext(component: string) {\n  const context = React.useContext(DatePickerContext);\n  if (!context) throw new Error(`${component} must be used inside DatePicker or DateRangePicker`);\n  return context;\n}\n\ntype PickerExtraProps = DateConstraintProps & {\n  className?: string;\n  /**\n   * Defaults to an `InputGroup` with the date input(s) and a calendar\n   * trigger, plus `DatePickerContent`. Compose your own to add a `Label`,\n   * description or a different layout.\n   */\n  children?: React.ReactNode;\n};\n\n/**\n * Typed segmented input plus a calendar popover over one value. `granularity`\n * `\"month\" | \"year\"` narrows both the segments and the calendar to whole\n * periods. The popover is non-modal: the input stays usable while it's open\n * and the calendar follows what's typed.\n */\nfunction DatePicker<T extends DateValue>({\n  granularity,\n  unavailableRanges,\n  minValue,\n  maxValue,\n  isDateUnavailable,\n  placeholderValue,\n  value: valueProp,\n  defaultValue,\n  onChange,\n  className,\n  children,\n  ...props\n}: Omit<AriaDatePickerProps<T>, \"granularity\" | \"children\" | \"className\"> & PickerExtraProps) {\n  const constraints = useDateConstraints({\n    granularity,\n    minValue,\n    maxValue,\n    isDateUnavailable,\n    unavailableRanges,\n    placeholderValue,\n  });\n  const { period } = constraints;\n  const [value, setValue] = useControlledValue<DateValue | null>(\n    valueProp,\n    defaultValue,\n    onChange as ((value: DateValue | null) => void) | undefined,\n  );\n  const handleChange = (next: DateValue | null) =>\n    setValue(next && period !== \"day\" ? periodStart(next, period) : next);\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const isEditing = useFocusWithin(rootRef);\n  const dates = React.useMemo(() => (value ? [value] : []), [value]);\n  useBlockedValueCorrection({\n    dates,\n    isEditing,\n    period,\n    minValue,\n    maxValue,\n    unavailableRanges,\n    onClear: () => setValue(null),\n    onCorrect: ([date]) => setValue(date ?? null),\n  });\n\n  const context = React.useMemo<DatePickerContextValue>(\n    () => ({\n      isRange: false,\n      period,\n      minValue,\n      maxValue,\n      isDateUnavailable,\n      unavailableRanges,\n      rootRef,\n    }),\n    [period, minValue, maxValue, isDateUnavailable, unavailableRanges],\n  );\n\n  return (\n    <AriaDatePicker\n      {...(props as AriaDatePickerProps<DateValue>)}\n      ref={rootRef}\n      data-slot=\"date-picker\"\n      value={value ?? null}\n      onChange={handleChange}\n      granularity={constraints.granularity}\n      minValue={constraints.minValue}\n      maxValue={constraints.maxValue}\n      isDateUnavailable={constraints.isDateUnavailable}\n      placeholderValue={constraints.placeholderValue}\n      className={cn(\"group/date-picker flex flex-col gap-2\", className)}\n    >\n      <DatePickerContext.Provider value={context}>\n        {children ?? (\n          <>\n            <InputGroup>\n              <DateInput />\n              <InputGroupAddon align=\"inline-end\">\n                <DatePickerTrigger />\n              </InputGroupAddon>\n            </InputGroup>\n            <DatePickerContent />\n          </>\n        )}\n      </DatePickerContext.Provider>\n    </AriaDatePicker>\n  );\n}\n\n/**\n * Range version of `DatePicker`: two segmented inputs (`slot=\"start\"` /\n * `slot=\"end\"`) and a `RangeCalendar`. With `granularity=\"month\" | \"year\"`\n * the range runs from the start period's first day to the end period's last\n * day. Without `DatePickerContent` it's a plain range field. Custom children\n * must keep both inputs inside an `InputGroup` (a react-aria `Group`): the\n * picker moves focus between their segments through it.\n */\nfunction DateRangePicker<T extends DateValue>({\n  granularity,\n  unavailableRanges,\n  minValue,\n  maxValue,\n  isDateUnavailable,\n  placeholderValue,\n  value: valueProp,\n  defaultValue,\n  onChange,\n  className,\n  children,\n  ...props\n}: Omit<AriaDateRangePickerProps<T>, \"granularity\" | \"children\" | \"className\"> & PickerExtraProps) {\n  const constraints = useDateConstraints({\n    granularity,\n    minValue,\n    maxValue,\n    isDateUnavailable,\n    unavailableRanges,\n    placeholderValue,\n  });\n  const { period } = constraints;\n  const [value, setValue] = useControlledValue<RangeValue<DateValue> | null>(\n    valueProp,\n    defaultValue,\n    onChange as ((value: RangeValue<DateValue> | null) => void) | undefined,\n  );\n  const handleChange = (next: RangeValue<DateValue> | null) =>\n    setValue(\n      next && period !== \"day\"\n        ? { start: periodStart(next.start, period), end: periodEnd(next.end, period) }\n        : next,\n    );\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const isEditing = useFocusWithin(rootRef);\n  const dates = React.useMemo(() => (value ? [value.start, value.end] : []), [value]);\n  useBlockedValueCorrection({\n    dates,\n    isEditing,\n    isRange: true,\n    period,\n    minValue,\n    maxValue,\n    unavailableRanges,\n    onClear: () => setValue(null),\n    onCorrect: ([start, end = start]) => {\n      if (start && end) setValue({ start, end });\n    },\n  });\n\n  const calendarUnavailable = React.useMemo(\n    () => isDateUnavailable && ((date: DateValue) => isDateUnavailable(date, null)),\n    [isDateUnavailable],\n  );\n  const context = React.useMemo<DatePickerContextValue>(\n    () => ({\n      isRange: true,\n      period,\n      minValue,\n      maxValue,\n      isDateUnavailable: calendarUnavailable,\n      unavailableRanges,\n      rootRef,\n    }),\n    [period, minValue, maxValue, calendarUnavailable, unavailableRanges],\n  );\n\n  return (\n    <AriaDateRangePicker\n      {...(props as AriaDateRangePickerProps<DateValue>)}\n      ref={rootRef}\n      data-slot=\"date-range-picker\"\n      value={value ?? null}\n      onChange={handleChange}\n      granularity={constraints.granularity}\n      minValue={constraints.minValue}\n      maxValue={constraints.maxValue}\n      isDateUnavailable={constraints.isDateUnavailable}\n      placeholderValue={constraints.placeholderValue}\n      className={cn(\"group/date-picker flex flex-col gap-2\", className)}\n    >\n      <DatePickerContext.Provider value={context}>\n        {children ?? (\n          <>\n            <InputGroup>\n              <DateInput slot=\"start\" />\n              <span aria-hidden=\"true\" className=\"text-muted-foreground\">\n                –\n              </span>\n              <DateInput slot=\"end\" />\n              <InputGroupAddon align=\"inline-end\" className=\"ml-auto\">\n                <DatePickerTrigger />\n              </InputGroupAddon>\n            </InputGroup>\n            <DatePickerContent />\n          </>\n        )}\n      </DatePickerContext.Provider>\n    </AriaDateRangePicker>\n  );\n}\n\n/** Calendar button for an `InputGroupAddon`; react-aria labels and wires it. */\nfunction DatePickerTrigger({ children, ...props }: React.ComponentProps<typeof InputGroupButton>) {\n  useDatePickerContext(\"DatePickerTrigger\");\n  return (\n    <InputGroupButton data-slot=\"date-picker-trigger\" size=\"icon-xs\" {...props}>\n      {children ?? <CalendarIcon />}\n    </InputGroupButton>\n  );\n}\n\n/**\n * The calendar popover. Non-modal on purpose: a modal popover makes the rest\n * of the page inert, so the paired input couldn't be clicked or typed into\n * while the calendar is open. Interacting with the picker itself keeps it\n * open; anything else outside closes it, as do Escape and choosing a date.\n */\nfunction DatePickerContent({\n  className,\n  placement = \"bottom start\",\n  ...props\n}: Omit<React.ComponentProps<typeof Popover>, \"children\">) {\n  const context = useDatePickerContext(\"DatePickerContent\");\n  const state = React.useContext(OverlayTriggerStateContext);\n  const dialogContext = React.useContext(DialogContext);\n  // id/aria-labelledby react-aria prepared for the picker's dialog.\n  const dialogProps =\n    dialogContext && !(\"slots\" in dialogContext)\n      ? (dialogContext as React.HTMLAttributes<HTMLElement>)\n      : {};\n  // Our Popover doesn't take a ref; the dialog fills it (padding lives on the dialog).\n  const dialogRef = React.useRef<HTMLDivElement>(null);\n  const { rootRef } = context;\n  const isOpen = state?.isOpen ?? false;\n  const close = state?.close;\n\n  const isInsidePicker = React.useCallback(\n    (target: EventTarget | null) =>\n      target instanceof Node &&\n      (!!rootRef.current?.contains(target) || !!dialogRef.current?.contains(target)),\n    [rootRef],\n  );\n\n  // Non-modal popovers aren't dismissed by outside presses, so do it here.\n  React.useEffect(() => {\n    if (!isOpen || !close) return;\n    const onPointerDown = (event: PointerEvent) => {\n      if (!isInsidePicker(event.target)) close();\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown, true);\n    return () => document.removeEventListener(\"pointerdown\", onPointerDown, true);\n  }, [isOpen, close, isInsidePicker]);\n\n  const calendarProps = {\n    granularity: context.period,\n    minValue: context.minValue,\n    maxValue: context.maxValue,\n    isDateUnavailable: context.isDateUnavailable,\n    unavailableRanges: context.unavailableRanges,\n    // The picker corrects blocked values itself, once its input loses focus.\n    correctBlockedValue: false,\n  };\n\n  return (\n    <Popover\n      placement={placement}\n      isNonModal\n      shouldCloseOnInteractOutside={(element) => !isInsidePicker(element)}\n      className={cn(\"w-auto overflow-auto p-0\", className)}\n      {...props}\n    >\n      {/* A plain role=\"dialog\" instead of react-aria's <Dialog>: useDialog\n          turns on the overlay's focus containment even in a non-modal\n          popover, which would yank focus back from the input. */}\n      <div\n        ref={dialogRef}\n        role=\"dialog\"\n        id={dialogProps.id}\n        aria-labelledby={dialogProps[\"aria-labelledby\"]}\n        aria-label={dialogProps[\"aria-label\"]}\n        className=\"p-1 outline-none\"\n      >\n        {context.isRange ? <RangeCalendar {...calendarProps} /> : <Calendar {...calendarProps} />}\n      </div>\n    </Popover>\n  );\n}\n\nexport { DatePicker, DatePickerContent, DatePickerTrigger, DateRangePicker };\n",
      "type": "registry:component",
      "target": "components/fma-ui/date-picker.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "tags": [
      "form",
      "date",
      "react-aria"
    ]
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}