{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"date-picker","title":"Date Picker","description":"Button-triggered, locale-aware single-date picker with Calendar overlay.","dependencies":["@internationalized/date@3.12.3","lucide-react@1.18.0","react-aria-components@1.20.0"],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/calendar","@siteplane/input","@siteplane/locale"],"files":[{"path":"packages/ui/src/components/date-picker/date-picker.tsx","content":"\"use client\";\n\nimport { type DateValue } from \"@internationalized/date\";\nimport { CalendarIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  Button as DatePickerButtonPrimitive,\n  DatePicker as DatePickerPrimitive,\n  type DatePickerState,\n  Dialog as DatePickerDialogPrimitive,\n  Group as DatePickerGroupPrimitive,\n  Popover as DatePickerPopoverPrimitive,\n  useLocale,\n} from \"react-aria-components\";\n\nimport { isInvalidAriaValue } from \"../../system/form/aria-invalid\";\nimport { useFormReset } from \"../../system/form/use-form-reset\";\nimport { normalizeFormattedText } from \"../../system/locale/formatted-text\";\nimport { SiteplaneLocaleProvider } from \"../../system/locale/siteplane-locale-provider\";\nimport {\n  type SiteplaneControlSize,\n  SiteplaneUIPortalScope,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  surfaceClassNames,\n  type FormControlTextSize,\n} from \"../../system/styles/surface\";\nimport { cn } from \"../../system/utils/cn\";\nimport { Button } from \"../button\";\nimport { Calendar } from \"../calendar\";\nimport {\n  calendarMonthBounds,\n  type CalendarOptions,\n  fromNativeDate,\n  isCalendarDayDisabled,\n  toNativeDate,\n} from \"../calendar/calendar\";\nimport {\n  useFieldControlBridge,\n  useFieldControlProxy,\n} from \"../field/field-control-bridge\";\nimport { ExternalInputOwner, Input } from \"../input/input\";\n\nimport \"./date-picker.css\";\n\nexport type DatePickerProps = {\n  \"aria-describedby\"?: string | undefined;\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"];\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string | undefined;\n  calendarProps?: CalendarOptions;\n  className?: string;\n  defaultValue?: Date;\n  disabled?: boolean;\n  /** The id of the form the date belongs to, like DateField. */\n  form?: string;\n  formatValue?: ((value: Date | undefined) => React.ReactNode) | undefined;\n  locale?: string;\n  /** Submits the date as `YYYY-MM-DD` under this name, like DateField. */\n  name?: string;\n  onValueChange?: (value: Date | undefined) => void;\n  placeholder?: React.ReactNode;\n  /** A form is not submitted while the picker holds no date. */\n  required?: boolean;\n  /** The control size, like the other date and time fields. */\n  size?: SiteplaneControlSize;\n  /**\n   * The trigger text: unset is 14 px, or 16 px at `xl`; `sm` and `base` set\n   * 14 px and 16 px at every size.\n   */\n  textSize?: FormControlTextSize;\n  value?: Date | null;\n};\n\nfunction toCalendarDate(value: Date | null | undefined) {\n  return value ? fromNativeDate(value) : null;\n}\n\nfunction toOptionalNativeDate(\n  value: DateValue | null | undefined,\n): Date | undefined {\n  return value ? toNativeDate(value) : undefined;\n}\n\nfunction formatDatePickerValue(value: DateValue | null | undefined): string {\n  if (!value) return \"\";\n  return `${String(value.year).padStart(4, \"0\")}-${String(value.month).padStart(2, \"0\")}-${String(value.day).padStart(2, \"0\")}`;\n}\n\nfunction noop(): void {}\n\nexport function DatePicker(props: DatePickerProps): React.ReactElement {\n  const picker = <DatePickerRoot {...props} />;\n\n  return props.locale ? (\n    <SiteplaneLocaleProvider locale={props.locale}>\n      {picker}\n    </SiteplaneLocaleProvider>\n  ) : (\n    picker\n  );\n}\n\nfunction DatePickerRoot({\n  \"aria-describedby\": ariaDescribedBy,\n  \"aria-invalid\": ariaInvalid,\n  \"aria-label\": ariaLabel = \"Choose date\",\n  \"aria-labelledby\": ariaLabelledBy,\n  calendarProps,\n  className,\n  defaultValue,\n  disabled,\n  form,\n  formatValue,\n  name,\n  onValueChange,\n  placeholder = \"Choose date\",\n  required,\n  size,\n  textSize,\n  value,\n}: DatePickerProps): React.ReactElement {\n  const controlled = value !== undefined;\n  const groupRef = React.useRef<HTMLDivElement>(null);\n  const field = useFieldControlBridge({\n    \"aria-describedby\": ariaDescribedBy,\n    \"aria-labelledby\": ariaLabelledBy,\n    disabled,\n    name,\n  });\n  // React Aria validates the date unless the application controls the invalid\n  // state through aria-invalid or an invalid Field.\n  const controlledInvalid =\n    ariaInvalid !== undefined\n      ? isInvalidAriaValue(ariaInvalid)\n      : field.invalid || undefined;\n  // A required picker outside a Field that stopped a form submission.\n  const [missing, setMissing] = React.useState(false);\n  const invalid = controlledInvalid ?? (field.valid === false || missing);\n  const focusTrigger = () =>\n    groupRef.current?.querySelector<HTMLElement>(\n      '[data-slot=\"date-picker-trigger\"]',\n    ) ?? null;\n  // The React Aria trigger drops aria-invalid, so the state is set on the\n  // rendered button.\n  React.useLayoutEffect(() => {\n    const trigger = groupRef.current?.querySelector(\n      '[data-slot=\"date-picker-trigger\"]',\n    );\n    if (invalid) trigger?.setAttribute(\"aria-invalid\", \"true\");\n    else trigger?.removeAttribute(\"aria-invalid\");\n  });\n  const { locale } = useLocale();\n  const shortDateFormatter = React.useMemo(\n    () =>\n      new Intl.DateTimeFormat(locale, {\n        day: \"numeric\",\n        month: \"short\",\n        year: \"numeric\",\n      }),\n    [locale],\n  );\n\n  // React Aria's optional props reject `undefined` under\n  // exactOptionalPropertyTypes, so unset values are omitted.\n  return (\n    <DatePickerPrimitive\n      {...(field.describedBy === undefined\n        ? {}\n        : { \"aria-describedby\": field.describedBy })}\n      {...(field.labelledBy ? {} : { \"aria-label\": ariaLabel })}\n      {...(field.labelledBy === undefined\n        ? {}\n        : { \"aria-labelledby\": field.labelledBy })}\n      className=\"sp-date-picker sp-base-date-picker contents\"\n      data-foundation=\"date-picker\"\n      data-slot=\"date-picker\"\n      {...(controlled\n        ? { value: toCalendarDate(value) }\n        : { defaultValue: toCalendarDate(defaultValue) })}\n      isDateUnavailable={(date) =>\n        isCalendarDayDisabled(toNativeDate(date), calendarProps?.disabled)\n      }\n      isDisabled={field.disabled}\n      {...(controlledInvalid === undefined\n        ? {}\n        : { isInvalid: controlledInvalid })}\n      {...(required === undefined ? {} : { isRequired: required })}\n      {...calendarMonthBounds(calendarProps ?? {})}\n      onChange={(nextValue) => {\n        if (nextValue) setMissing(false);\n        onValueChange?.(toOptionalNativeDate(nextValue));\n      }}\n      shouldCloseOnSelect\n    >\n      {({ state }) => {\n        const nativeValue = toOptionalNativeDate(state.value);\n        const label = formatValue\n          ? formatValue(nativeValue)\n          : nativeValue\n            ? normalizeFormattedText(shortDateFormatter.format(nativeValue))\n            : placeholder;\n\n        return (\n          <>\n            <DatePickerGroupPrimitive\n              className={cn(\"inline-flex max-w-full\", className)}\n              ref={groupRef}\n            >\n              <Button\n                className=\"w-full justify-start\"\n                data-invalid={invalid || undefined}\n                data-slot=\"date-picker-trigger\"\n                data-text-size={textSize}\n                disabled={field.disabled}\n                render={<DatePickerButtonPrimitive />}\n                size={size}\n                variant=\"outline\"\n              >\n                <CalendarIcon aria-hidden=\"true\" />\n                <span\n                  className={cn(\n                    \"sp-date-picker-value\",\n                    !nativeValue && \"text-muted-foreground\",\n                  )}\n                  data-slot=\"date-picker-value\"\n                >\n                  {label}\n                </span>\n              </Button>\n            </DatePickerGroupPrimitive>\n            <DatePickerPopoverPrimitive\n              className={cn(\n                surfaceClassNames.floatingOverlay,\n                \"sp-date-picker-popover\",\n              )}\n              data-slot=\"date-picker-popover\"\n              offset={4}\n              placement=\"bottom start\"\n            >\n              <SiteplaneUIPortalScope>\n                <DatePickerDialogPrimitive\n                  className=\"sp-date-picker-dialog outline-none\"\n                  data-slot=\"date-picker-dialog\"\n                >\n                  <Calendar\n                    {...calendarProps}\n                    aria-label={ariaLabel}\n                    data-slot=\"date-picker-calendar\"\n                  />\n                </DatePickerDialogPrimitive>\n              </SiteplaneUIPortalScope>\n            </DatePickerPopoverPrimitive>\n            <DatePickerFormValue\n              controlled={controlled}\n              controlledInvalid={controlledInvalid}\n              defaultValue={defaultValue}\n              disabled={field.disabled}\n              focusTarget={focusTrigger}\n              form={form}\n              name={field.name}\n              onMissing={() => {\n                setMissing(true);\n                focusTrigger()?.focus();\n              }}\n              required={required}\n              state={state}\n            />\n          </>\n        );\n      }}\n    </DatePickerPrimitive>\n  );\n}\n\n/**\n * The picker's form value, as DateField submits it: `YYYY-MM-DD` under `name`,\n * empty while no date is chosen. Inside a Field a proxy input registers the\n * picker with the Field and Form, which check `required`. Outside a Field a\n * visually hidden text input checks it, since a hidden input takes no part in\n * constraint validation.\n */\nfunction DatePickerFormValue({\n  controlled,\n  controlledInvalid,\n  defaultValue,\n  disabled,\n  focusTarget,\n  form,\n  name,\n  onMissing,\n  required,\n  state,\n}: {\n  controlled: boolean;\n  controlledInvalid: boolean | undefined;\n  defaultValue: Date | undefined;\n  disabled: boolean;\n  focusTarget: () => HTMLElement | null;\n  form: string | undefined;\n  name: string | undefined;\n  onMissing: () => void;\n  required: boolean | undefined;\n  state: DatePickerState;\n}): React.ReactElement {\n  const resetRef = useFormReset<HTMLInputElement>(form, () => {\n    if (!controlled) state.setValue(toCalendarDate(defaultValue));\n  });\n  const value = formatDatePickerValue(state.value);\n  const proxyProps = useFieldControlProxy({\n    controlled: controlledInvalid !== undefined,\n    disabled,\n    displayValidation: state.displayValidation,\n    focusTarget,\n    form,\n    name,\n    onRevalidate: state.commitValidation,\n    realtimeValidation: state.realtimeValidation,\n    required,\n    value,\n  });\n\n  return (\n    <>\n      <Input\n        disabled={disabled}\n        form={form}\n        name={name}\n        readOnly\n        ref={resetRef}\n        required={required}\n        type=\"hidden\"\n        value={value}\n      />\n      {proxyProps ? (\n        <ExternalInputOwner>\n          <Input nativeInput unstyled {...proxyProps} />\n        </ExternalInputOwner>\n      ) : required ? (\n        <ExternalInputOwner>\n          <Input\n            aria-hidden=\"true\"\n            className=\"sr-only\"\n            disabled={disabled}\n            form={form}\n            nativeInput\n            onChange={noop}\n            onInvalid={(event) => {\n              event.preventDefault();\n              onMissing();\n            }}\n            required\n            tabIndex={-1}\n            type=\"text\"\n            unstyled\n            value={value}\n          />\n        </ExternalInputOwner>\n      ) : null}\n    </>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/date-picker/date-picker.tsx"},{"path":"packages/ui/src/components/date-picker/date-picker.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* An invalid trigger takes the field error contour, like an invalid Select\n   * trigger. */\n  :root [data-chassis].sp-date-picker-trigger[data-invalid] {\n    --sp-rw-button-rim: inset 0 0 0 var(--sp-rw-border-default)\n      light-dark(\n        oklch(67.3% 0.2102 26.56 / 40%),\n        oklch(76.15% 0.1564 26.56 / 40%)\n      );\n  }\n\n  /* The trigger takes the field text scale of the other date and time fields:\n   * the Button's 14 px, or 16 px at xl, unless textSize sets one size for every\n   * control size. */\n  :root [data-chassis].sp-date-picker-trigger[data-text-size=\"sm\"] {\n    font-size: var(--sp-rw-font-14);\n    line-height: var(--sp-rw-line-21);\n  }\n\n  :root [data-chassis].sp-date-picker-trigger[data-text-size=\"base\"] {\n    font-size: var(--sp-rw-font-16);\n    line-height: var(--sp-rw-line-24);\n  }\n\n  /* The surface (border, radius, shadow) is the floating-md recipe that\n   * design-rework.css draws for the popover's data-slot. */\n  .sp-date-picker-popover {\n    z-index: 50;\n    width: max-content;\n    padding: var(--sp-rw-space-4);\n    outline: none;\n    transform-origin: var(--trigger-anchor-point);\n  }\n\n  .sp-date-picker-popover[data-entering] {\n    animation: sp-date-picker-enter var(--motion-duration-overlay)\n      var(--motion-ease-standard) both;\n  }\n\n  .sp-date-picker-popover[data-exiting] {\n    animation: sp-date-picker-exit var(--motion-duration-fast)\n      var(--motion-ease-soft-in-out) both;\n  }\n\n  @keyframes sp-date-picker-enter {\n    from {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  @keyframes sp-date-picker-exit {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-date-picker-popover[data-entering],\n    .sp-date-picker-popover[data-exiting] {\n      animation-duration: 1ms;\n    }\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-date-picker-popover[data-entering],\n  html[data-motion-reduce=\"true\"] .sp-date-picker-popover[data-exiting],\n  [data-sp-motion=\"reduced\"] .sp-date-picker-popover[data-entering],\n  [data-sp-motion=\"reduced\"] .sp-date-picker-popover[data-exiting] {\n    animation-duration: 1ms;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/date-picker/date-picker.css"},{"path":"packages/ui/src/components/date-picker/index.ts","content":"export * from \"./date-picker\";\n","type":"registry:ui","target":"~/src/siteplane/components/date-picker/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/date-picker.md","categories":["Date & color"],"meta":{"avoidWhen":["Avoid when direct segmented date entry is required; use DateField instead."],"description":"Button-triggered, locale-aware single-date picker with Calendar overlay.","knownLimitations":["Values use native Date calendar fields in the local time zone; the locale changes formatting, not the zone.","Formatting changes only the visible label; the form value stays YYYY-MM-DD.","No readOnly prop: React Aria disables the trigger of a read-only picker, so it would look disabled. Use DateField for a read-only date."],"providerNotes":["Use the locale prop for this picker, or SiteplaneLocaleProvider for a shared locale. SiteplaneUIProvider configures visual defaults, not locale."],"releaseStatus":"stable","searchTerms":["date","picker","calendar"],"useWhen":["Use when one date should be selected from a compact Calendar disclosure."]}}