{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-picker",
  "type": "registry:ui",
  "title": "Date picker",
  "description": "Choose a date without losing context.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/calendar.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/date-picker/date-picker.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport type { ButtonHTMLAttributes, FocusEvent, KeyboardEvent } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { Variants } from \"motion/react\";\nimport { CalendarDays, ChevronDown } from \"lucide-react\";\nimport { Calendar, type CalendarDateMatcher } from \"../calendar/calendar\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface DatePickerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"value\" | \"onChange\"> {\n  label: string;\n  value?: Date;\n  onChange?: (date: Date | undefined) => void;\n  description?: string;\n  placeholder?: string;\n  minDate?: Date;\n  maxDate?: Date;\n  disabledDates?: CalendarDateMatcher;\n  locale?: string;\n  format?: Intl.DateTimeFormatOptions;\n  /** Adds a Today button to the calendar header. */\n  showToday?: boolean;\n}\n\n/* The trigger anchors the calendar, so press feedback stays in colour; it never scales. */\nconst triggerClass =\n  \"relative flex min-h-control-md w-full items-center gap-[9px] rounded-control border border-line bg-surface px-3 text-left text-(length:--text-sm) text-ink [transition:border-color_var(--duration-fast)_var(--ease-standard),background_var(--duration-fast)_var(--ease-standard),box-shadow_var(--duration-fast)_var(--ease-standard)] enabled:hover:border-line-strong enabled:hover:bg-hover enabled:active:border-line-strong enabled:active:bg-hover aria-expanded:border-line-strong aria-expanded:bg-hover disabled:cursor-not-allowed disabled:opacity-50 [&>svg:first-child]:flex-[0_0_auto] [&>svg:first-child]:text-ink-3 motion-reduce:transition-none\";\n/* Grows out of the trigger's calendar icon and returns there on close. On phone widths it matches the field instead of running\n   past the screen edge, and a tighter inset keeps the month and its controls on one row. */\nconst popoverClass =\n  \"absolute top-[calc(100%+8px)] left-0 z-20 w-[min(320px,calc(100vw-32px))] origin-[22px_-8px] overflow-hidden rounded-panel border border-line bg-surface p-4 shadow-floating will-change-[transform,opacity] max-[361px]:right-0 max-[361px]:w-auto max-[361px]:p-3\";\nconst footerButton =\n  \"rounded-button bg-transparent px-2.5 py-1 text-ink-2 [transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),transform_var(--duration-spring)_var(--ease-spring)] enabled:hover:bg-hover enabled:hover:text-ink enabled:active:ease-standard enabled:active:[transform:scale(.97)] enabled:active:[transition-duration:var(--duration-fast),var(--duration-fast),100ms] disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:transition-none motion-reduce:enabled:active:[transform:none]\";\n\nconst monthStart = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1);\nconst { spring, duration, ease, blur } = motionTokens;\nconst instant = { duration: duration.instant };\n/** Each part of the date rolls with time: a later date rises from below, an earlier one drops from above. Parts that did not change stay still. */\nconst valueRoll: Variants = {\n  enter: (direction: number) => ({ opacity: 0, y: `${direction * 0.35}em`, filter: `blur(${blur.soft}px)` }),\n  center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: duration.standard, ease: ease.enter } },\n  exit: (direction: number) => ({\n    opacity: 0,\n    y: `${direction * -0.3}em`,\n    filter: `blur(${blur.subtle}px)`,\n    transition: { duration: 0.14, ease: ease.standard },\n  }),\n};\n/** Reduced motion keeps a short crossfade; resting values match the roll so server and client markup agree. */\nconst valueFade: Variants = {\n  enter: { opacity: 0 },\n  center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: instant },\n  exit: { opacity: 0, transition: instant },\n};\n/** The footer confirms the pick: new copy rises in with a soft blur while the old line lifts away. */\nconst statusRise: Variants = {\n  enter: { opacity: 0, y: \"0.3em\", filter: `blur(${blur.soft}px)` },\n  center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: duration.standard, ease: ease.enter } },\n  exit: {\n    opacity: 0,\n    y: \"-0.3em\",\n    filter: `blur(${blur.subtle}px)`,\n    transition: { duration: 0.14, ease: ease.standard },\n  },\n};\n\nexport function DatePicker({\n  label,\n  value,\n  onChange,\n  description,\n  placeholder = \"Select a date\",\n  minDate,\n  maxDate,\n  disabledDates,\n  locale = \"en-US\",\n  format = { month: \"short\", day: \"numeric\", year: \"numeric\" },\n  showToday,\n  id,\n  className,\n  disabled,\n  ...buttonProps\n}: DatePickerProps) {\n  const generatedId = useId();\n  const controlId = id ?? generatedId;\n  const hintId = description ? `${controlId}-description` : undefined;\n  const [open, setOpen] = useState(false);\n  const [month, setMonth] = useState(() => monthStart(value ?? new Date()));\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const popoverRef = useRef<HTMLDivElement>(null);\n  const closeTimer = useRef<number | undefined>(undefined);\n  const formatter = new Intl.DateTimeFormat(locale, format);\n  const reduce = useReducedMotion() ?? false;\n  const time = value?.getTime() ?? null;\n  const [previousTime, setPreviousTime] = useState(time);\n  const [direction, setDirection] = useState(1);\n  if (previousTime !== time) {\n    setPreviousTime(time);\n    setDirection(time === null || previousTime === null || time >= previousTime ? 1 : -1);\n  }\n  const shown = value ? formatter.format(value) : placeholder;\n  const parts = value ? formatter.formatToParts(value) : [];\n  // Parts after a wider or narrower segment move on the same curve the new text arrives on, so they never overlap it.\n  const partMotion = reduce ? { duration: 0 } : { duration: duration.standard, ease: ease.enter };\n\n  useEffect(() => {\n    const onPointerDown = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) {\n        window.clearTimeout(closeTimer.current);\n        setOpen(false);\n      }\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointerDown);\n      window.clearTimeout(closeTimer.current);\n    };\n  }, []);\n\n  /** Opening moves focus to the day that owns the tab stop, so arrows work at once; a pointer open shows no ring. */\n  useEffect(() => {\n    if (open)\n      popoverRef.current\n        ?.querySelector<HTMLButtonElement>('[data-present] [data-date][tabindex=\"0\"]')\n        ?.focus({ preventScroll: true });\n  }, [open]);\n\n  /** Closing hands focus back to the trigger when it was inside the calendar, so keyboard users never land on the page body. */\n  const close = () => {\n    window.clearTimeout(closeTimer.current);\n    if (popoverRef.current?.contains(document.activeElement)) triggerRef.current?.focus();\n    setOpen(false);\n  };\n  /** The calendar always opens on the month of the current value, or today's month. */\n  const show = () => {\n    window.clearTimeout(closeTimer.current);\n    setMonth(monthStart(value ?? new Date()));\n    setOpen(true);\n  };\n  /** A picked day lets the highlight glide onto it and the footer confirm it, then the calendar returns to the field. */\n  const selectDate = (date: Date | undefined) => {\n    onChange?.(date);\n    window.clearTimeout(closeTimer.current);\n    if (reduce || !date) close();\n    else closeTimer.current = window.setTimeout(close, 300);\n  };\n  const onTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if ((event.key === \"ArrowDown\" || event.key === \"Enter\" || event.key === \" \") && !open) {\n      event.preventDefault();\n      show();\n    }\n    if (event.key === \"Escape\") close();\n  };\n  /** Tabbing past the popover closes it; focus moving within the field keeps it open. */\n  const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n    const next = event.relatedTarget as Node | null;\n    if (open && next && !rootRef.current?.contains(next)) {\n      window.clearTimeout(closeTimer.current);\n      setOpen(false);\n    }\n  };\n\n  return (\n    <div className={cn(\"grid min-w-0 gap-[7px]\", className)} ref={rootRef} onBlur={onBlur}>\n      <label className=\"text-sm leading-body font-medium text-ink\" htmlFor={controlId}>\n        {label}\n      </label>\n      <div className=\"peer/anchor relative min-w-0\">\n        <button\n          {...buttonProps}\n          ref={triggerRef}\n          id={controlId}\n          type=\"button\"\n          disabled={disabled}\n          aria-haspopup=\"dialog\"\n          aria-expanded={open}\n          aria-describedby={hintId}\n          className={triggerClass}\n          onClick={() => (open ? close() : show())}\n          onKeyDown={onTriggerKeyDown}\n        >\n          <CalendarDays size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n          <span className=\"sr-only\">{shown}</span>\n          <span className=\"relative flex min-w-0 flex-1\" aria-hidden=\"true\">\n            <AnimatePresence mode=\"popLayout\" initial={false} custom={1}>\n              {value ? (\n                <motion.span\n                  key=\"value\"\n                  className=\"relative inline-flex whitespace-pre tabular-nums\"\n                  custom={1}\n                  variants={reduce ? valueFade : valueRoll}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                >\n                  {parts.map((part, index) => (\n                    <motion.span\n                      key={`${index}-${part.type}`}\n                      className=\"relative inline-flex whitespace-pre\"\n                      layout=\"position\"\n                      layoutDependency={time}\n                      transition={partMotion}\n                    >\n                      {part.type === \"literal\" ? (\n                        <span className=\"inline-block whitespace-pre\">{part.value}</span>\n                      ) : (\n                        <AnimatePresence mode=\"popLayout\" initial={false} custom={direction}>\n                          <motion.span\n                            key={part.value}\n                            className=\"inline-block whitespace-pre\"\n                            custom={direction}\n                            variants={reduce ? valueFade : valueRoll}\n                            initial=\"enter\"\n                            animate=\"center\"\n                            exit=\"exit\"\n                          >\n                            {part.value}\n                          </motion.span>\n                        </AnimatePresence>\n                      )}\n                    </motion.span>\n                  ))}\n                </motion.span>\n              ) : (\n                <motion.span\n                  key=\"placeholder\"\n                  className=\"min-w-0 truncate text-ink-3\"\n                  custom={-1}\n                  variants={reduce ? valueFade : valueRoll}\n                  initial=\"enter\"\n                  animate=\"center\"\n                  exit=\"exit\"\n                >\n                  {placeholder}\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </span>\n          <ChevronDown\n            className={cn(\n              \"flex-[0_0_auto] text-ink-3 transition-transform duration-spring ease-spring motion-reduce:transition-none\",\n              open && \"rotate-180\",\n            )}\n            size={16}\n            strokeWidth={1.75}\n            aria-hidden=\"true\"\n          />\n        </button>\n        <AnimatePresence>\n          {open && (\n            <motion.div\n              ref={popoverRef}\n              className={popoverClass}\n              role=\"dialog\"\n              aria-label={`${label} calendar`}\n              onKeyDown={(event) => {\n                if (event.key === \"Escape\") {\n                  event.stopPropagation();\n                  close();\n                }\n              }}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -8, scale: 0.95 }}\n              animate={{\n                opacity: 1,\n                y: 0,\n                scale: 1,\n                transition: reduce\n                  ? instant\n                  : { ...spring.snappy, opacity: { duration: duration.fast, ease: ease.enter } },\n              }}\n              exit={{\n                opacity: 0,\n                ...(reduce ? {} : { y: -6, scale: 0.97 }),\n                transition: { duration: 0.14, ease: ease.standard },\n              }}\n            >\n              <Calendar\n                value={value}\n                onChange={selectDate}\n                month={month}\n                onMonthChange={setMonth}\n                minDate={minDate}\n                maxDate={maxDate}\n                disabledDates={disabledDates}\n                locale={locale}\n                showToday={showToday}\n              />\n              <div className=\"mt-3.5 flex items-center justify-between gap-2 border-t border-line-soft pt-[11px] text-(length:--text-xs) text-ink-3\">\n                <button type=\"button\" className={footerButton} onClick={() => selectDate(undefined)} disabled={!value}>\n                  Clear\n                </button>\n                <span className=\"grid min-w-0 justify-items-end *:max-w-full *:truncate *:[grid-area:1/1]\">\n                  <AnimatePresence initial={false}>\n                    <motion.span\n                      key={time ?? \"none\"}\n                      variants={reduce ? valueFade : statusRise}\n                      initial=\"enter\"\n                      animate=\"center\"\n                      exit=\"exit\"\n                    >\n                      {value ? `Selected ${formatter.format(value)}` : \"Choose a day\"}\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n      {description && (\n        <span\n          id={hintId}\n          className=\"text-xs leading-body text-ink-3 transition-opacity duration-fast ease-standard peer-has-[[role=dialog]]/anchor:opacity-0 motion-reduce:transition-none\"\n        >\n          {description}\n        </span>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/date-picker/date-picker.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/date-picker",
  "meta": {
    "kind": "component",
    "group": "Inputs",
    "export": "DatePicker",
    "tags": [
      "date",
      "field"
    ],
    "docs": "https://irisui.dev/components/date-picker"
  }
}
