{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-field",
  "title": "Date Field",
  "author": "@fma-ui/ui",
  "description": "A segmented, keyboard-editable date input. `granularity` also accepts \"month\" and \"year\" for month- or year-only fields. Values blocked by `minValue`/`maxValue` or `unavailableRanges` move to the nearest available date once the field loses focus, so typing is never interrupted. Exports `DateInput` for composing date pickers.",
  "dependencies": [
    "@internationalized/date@^3.12.4",
    "cn@^0.4.0",
    "react-aria-components@^1.21.1"
  ],
  "registryDependencies": [
    "@fma-ui/use-available-date-correction",
    "@fma-ui/date-granularity",
    "@fma-ui/interval-helpers",
    "@fma-ui/types"
  ],
  "files": [
    {
      "path": "src/core/date-field/date-field.tsx",
      "content": "\"use client\";\n\nimport { type DateValue, getLocalTimeZone, today } from \"@internationalized/date\";\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport {\n  DateField as AriaDateField,\n  type DateFieldProps as AriaDateFieldProps,\n  DateInput as AriaDateInput,\n  type DateInputProps as AriaDateInputProps,\n  DateSegment as AriaDateSegment,\n  type DateSegmentProps as AriaDateSegmentProps,\n  composeRenderProps,\n} from \"react-aria-components\";\nimport { useAvailableDateCorrection } from \"@/hooks/use-available-date-correction\";\nimport {\n  createIsPeriodUnavailable,\n  type DateGranularity,\n  isPeriodGranularity,\n  periodBounds,\n  periodStart,\n} from \"@/lib/date-granularity\";\nimport { createIsWithinAnyInterval } from \"@/lib/interval-helpers\";\nimport type { Interval } from \"@/lib/types\";\n\n/**\n * `\"year\"`/`\"month\"` render only those segments and commit the period's\n * first day; `\"day\"` and the time granularities behave like react-aria's.\n */\ntype DateFieldGranularity = DateGranularity | \"hour\" | \"minute\" | \"second\";\n\n// react-aria's public `Granularity` type stops at \"day\", but its field state\n// builds the segment list (and the \"is complete\" check) from the slice\n// `[maxGranularity … granularity]` of year/month/day/…, and\n// `createPlaceholderDate` handles \"year\"/\"month\" explicitly — so a coarser\n// granularity yields a real year(/month)-only field, with the missing fields\n// filled from `placeholderValue` at commit time. Filtering segments in the\n// render layer instead would leave the state waiting forever for a day.\ntype AriaGranularity = NonNullable<AriaDateFieldProps<DateValue>[\"granularity\"]>;\n\ninterface DateConstraintProps {\n  granularity?: DateFieldGranularity;\n  /**\n   * Inclusive from/to spans that can't be picked (e.g. already-booked days),\n   * on top of `minValue`/`maxValue` and `isDateUnavailable`. A value that ends\n   * up blocked — typed, or left behind by changed constraints — travels to\n   * the nearest available date (or is cleared when none is left) once the\n   * field loses focus, so typing is never interrupted.\n   */\n  unavailableRanges?: Interval<DateValue>[];\n}\n\n/**\n * Resolves the props react-aria needs for a granularity: period-widened\n * min/max, an `isDateUnavailable` lifted to whole periods, and a placeholder\n * at the start of the current period (so a month field commits day 1).\n * Shared by `DateField`, `DatePicker` and `DateRangePicker`.\n */\nfunction useDateConstraints<A extends unknown[]>({\n  granularity,\n  minValue,\n  maxValue,\n  isDateUnavailable,\n  unavailableRanges,\n  placeholderValue,\n}: DateConstraintProps & {\n  minValue?: DateValue | null;\n  maxValue?: DateValue | null;\n  isDateUnavailable?: (date: DateValue, ...rest: A) => boolean;\n  placeholderValue?: DateValue | null;\n}) {\n  const period: DateGranularity = isPeriodGranularity(granularity) ? granularity : \"day\";\n  const bounds = React.useMemo(\n    () => periodBounds(period, minValue, maxValue),\n    [period, minValue, maxValue],\n  );\n  const isUnavailable = React.useMemo(() => {\n    const isInRanges = createIsWithinAnyInterval(unavailableRanges);\n    const isDayUnavailable =\n      isInRanges && isDateUnavailable\n        ? (date: DateValue, ...rest: A) => isDateUnavailable(date, ...rest) || isInRanges(date)\n        : (isDateUnavailable ?? isInRanges);\n    if (period === \"day\" || !isDayUnavailable) return isDayUnavailable;\n    // Periods ignore the range picker's anchor argument: a month is blocked\n    // only when every day in it is.\n    return createIsPeriodUnavailable(period, (date) =>\n      isDayUnavailable(date, ...([null] as unknown as A)),\n    );\n  }, [period, isDateUnavailable, unavailableRanges]);\n  const placeholder = React.useMemo(() => {\n    if (period === \"day\") return placeholderValue;\n    return periodStart(placeholderValue ?? today(getLocalTimeZone()), period);\n  }, [period, placeholderValue]);\n\n  return {\n    period,\n    granularity: granularity as AriaGranularity | undefined,\n    minValue: bounds.minValue,\n    maxValue: bounds.maxValue,\n    isDateUnavailable: isUnavailable,\n    placeholderValue: placeholder,\n  };\n}\n\n/**\n * Uncontrolled/controlled value in one: react-aria always receives a\n * controlled value, so corrections can be written back through `setValue`.\n */\nfunction useControlledValue<V>(\n  value: V | undefined,\n  defaultValue: V | undefined,\n  onChange: ((value: V) => void) | undefined,\n) {\n  const [internal, setInternal] = React.useState(defaultValue);\n  const isControlled = value !== undefined;\n  const current = isControlled ? value : internal;\n  const setValue = React.useCallback(\n    (next: V) => {\n      if (!isControlled) setInternal(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n  return [current, setValue] as const;\n}\n\n/**\n * Whether focus is inside `ref`'s element. Native focusin/focusout (not\n * React's synthetic events), so focus moving into a portaled popover — the\n * date picker's calendar — counts as leaving.\n */\nfunction useFocusWithin(ref: React.RefObject<HTMLElement | null>) {\n  const [isFocusWithin, setFocusWithin] = React.useState(false);\n  React.useEffect(() => {\n    const element = ref.current;\n    if (!element) return;\n    const onFocusIn = () => setFocusWithin(true);\n    const onFocusOut = (event: FocusEvent) => {\n      const next = event.relatedTarget;\n      if (!(next instanceof Node && element.contains(next))) setFocusWithin(false);\n    };\n    element.addEventListener(\"focusin\", onFocusIn);\n    element.addEventListener(\"focusout\", onFocusOut);\n    return () => {\n      element.removeEventListener(\"focusin\", onFocusIn);\n      element.removeEventListener(\"focusout\", onFocusOut);\n    };\n  }, [ref]);\n  return isFocusWithin;\n}\n\n/**\n * Travel/clear for a blocked value, deferred while the user is editing:\n * react-aria emits a value on every keystroke once all segments are filled\n * (typing \"2026\" passes through year 2, 20, 202…), so correcting eagerly\n * would fight the typing. Runs on blur and whenever the constraints change.\n */\nfunction useBlockedValueCorrection({\n  dates,\n  isEditing,\n  isRange,\n  period,\n  minValue,\n  maxValue,\n  unavailableRanges,\n  onClear,\n  onCorrect,\n}: {\n  dates: DateValue[];\n  isEditing: boolean;\n  isRange?: boolean;\n  period: DateGranularity;\n  minValue?: DateValue | null;\n  maxValue?: DateValue | null;\n  unavailableRanges?: Interval<DateValue>[];\n  onClear: () => void;\n  onCorrect: (dates: DateValue[]) => void;\n}) {\n  useAvailableDateCorrection({\n    // Travel/clear works on days; month/year values are validated per period.\n    value: !isEditing && period === \"day\" ? dates : undefined,\n    min: minValue ?? undefined,\n    max: maxValue ?? undefined,\n    unavailableRanges,\n    selectionMode: isRange ? \"range\" : \"single\",\n    clearValue: onClear,\n    setValue: onCorrect,\n  });\n}\n\nfunction DateField<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<AriaDateFieldProps<T>, \"granularity\" | \"children\" | \"className\"> &\n  DateConstraintProps & {\n    className?: string;\n    /** Defaults to a bare `<DateInput />`. */\n    children?: React.ReactNode;\n  }) {\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  return (\n    <AriaDateField\n      {...(props as AriaDateFieldProps<DateValue>)}\n      ref={rootRef}\n      data-slot=\"date-field\"\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-field flex flex-col gap-2\", className)}\n    >\n      {children ?? <DateInput />}\n    </AriaDateField>\n  );\n}\n\n// Standalone it draws its own border/ring; inside an InputGroup the group\n// draws them (it reacts to the segments' `data-slot=\"input-group-control\"`),\n// so the input sits flush and none of its own frame states apply.\nconst DATE_INPUT_CLASSES = cn(\n  \"flex h-8 w-full min-w-0 items-center text-base outline-none md:text-sm\",\n  \"not-in-data-[slot=input-group]:rounded-lg not-in-data-[slot=input-group]:border not-in-data-[slot=input-group]:border-input not-in-data-[slot=input-group]:bg-transparent not-in-data-[slot=input-group]:px-2.5 not-in-data-[slot=input-group]:py-1 not-in-data-[slot=input-group]:transition-colors dark:not-in-data-[slot=input-group]:bg-input/30\",\n  \"not-in-data-[slot=input-group]:data-focus-within:border-ring not-in-data-[slot=input-group]:data-focus-within:ring-3 not-in-data-[slot=input-group]:data-focus-within:ring-ring/50\",\n  \"not-in-data-[slot=input-group]:data-invalid:border-destructive not-in-data-[slot=input-group]:data-invalid:ring-3 not-in-data-[slot=input-group]:data-invalid:ring-destructive/20 dark:not-in-data-[slot=input-group]:data-invalid:border-destructive/50 dark:not-in-data-[slot=input-group]:data-invalid:ring-destructive/40\",\n  \"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 not-in-data-[slot=input-group]:data-disabled:bg-input/50\",\n  \"in-data-[slot=input-group]:h-full in-data-[slot=input-group]:px-2.5\",\n);\n\n/**\n * The segmented input of a `DateField`, `DatePicker` or `DateRangePicker`\n * (`slot=\"start\" | \"end\"` in a range). Fills the row inside an `InputGroup`,\n * except range inputs, which size to their content.\n */\nfunction DateInput({\n  className,\n  ...props\n}: Omit<AriaDateInputProps, \"children\" | \"className\"> & { className?: string }) {\n  return (\n    <AriaDateInput\n      data-slot=\"date-input\"\n      className={composeRenderProps(className, (className) =>\n        cn(\n          DATE_INPUT_CLASSES,\n          props.slot\n            ? \"in-data-[slot=input-group]:w-auto in-data-[slot=input-group]:flex-none in-data-[slot=input-group]:px-1 in-data-[slot=input-group]:first:pl-2.5\"\n            : \"in-data-[slot=input-group]:flex-1\",\n          className,\n        ),\n      )}\n      {...props}\n    >\n      {(segment) => <DateSegment segment={segment} />}\n    </AriaDateInput>\n  );\n}\n\nfunction DateSegment({ className, ...props }: AriaDateSegmentProps) {\n  return (\n    <AriaDateSegment\n      // Lets an enclosing InputGroup show its focus/invalid ring.\n      data-slot=\"input-group-control\"\n      className={composeRenderProps(className, (className) =>\n        cn(\n          \"rounded-sm px-0.5 tabular-nums caret-transparent outline-none select-none\",\n          \"data-placeholder:text-muted-foreground data-[type=literal]:px-0 data-[type=literal]:text-muted-foreground\",\n          \"data-focused:bg-muted data-focused:text-foreground data-invalid:text-destructive data-focused:data-invalid:text-destructive\",\n          className,\n        ),\n      )}\n      {...props}\n    />\n  );\n}\n\nexport type { DateConstraintProps, DateFieldGranularity };\nexport {\n  DateField,\n  DateInput,\n  DateSegment,\n  useBlockedValueCorrection,\n  useControlledValue,\n  useDateConstraints,\n  useFocusWithin,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ui/date-field.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "tags": [
      "form",
      "date",
      "react-aria"
    ]
  },
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}