{
  "$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://mcp.xzero.co.uk/r/xzero-motion-tokens.json",
    "https://mcp.xzero.co.uk/r/calendar.json"
  ],
  "files": [
    {
      "path": "registry/components/date-picker/date-picker.tsx",
      "type": "registry:component",
      "target": "@components/xzero/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 styles from \"./date-picker.module.css\";\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\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) => ({ opacity: 0, y: `${direction * -0.3}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: 0.14, ease: ease.standard } }),\n};\n/** Reduced motion keeps a short crossfade; resting values match the roll so server and client markup agree. */\nconst valueFade: Variants = { enter: { opacity: 0 }, center: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: instant }, exit: { opacity: 0, transition: instant } };\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: { opacity: 0, y: \"-0.3em\", filter: `blur(${blur.subtle}px)`, transition: { duration: 0.14, ease: ease.standard } },\n};\n\nexport function DatePicker({ label, value, onChange, description, placeholder = \"Select a date\", minDate, maxDate, disabledDates, locale = \"en-US\", format = { month: \"short\", day: \"numeric\", year: \"numeric\" }, showToday, id, className, disabled, ...buttonProps }: 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) { setPreviousTime(time); setDirection(time === null || previousTime === null || time >= previousTime ? 1 : -1); }\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) => { if (!rootRef.current?.contains(event.target as Node)) { window.clearTimeout(closeTimer.current); setOpen(false); } };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    return () => { document.removeEventListener(\"pointerdown\", onPointerDown); window.clearTimeout(closeTimer.current); };\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) popoverRef.current?.querySelector<HTMLButtonElement>(\"[data-present] [data-date][tabindex=\\\"0\\\"]\")?.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 = () => { window.clearTimeout(closeTimer.current); setMonth(monthStart(value ?? new Date())); setOpen(true); };\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) { event.preventDefault(); show(); }\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)) { window.clearTimeout(closeTimer.current); setOpen(false); }\n  };\n\n  return <div className={[styles.field, className].filter(Boolean).join(\" \")} ref={rootRef} onBlur={onBlur}>\n    <label className={styles.label} htmlFor={controlId}>{label}</label>\n    <div className={styles.anchor}>\n      <button {...buttonProps} ref={triggerRef} id={controlId} type=\"button\" disabled={disabled} aria-haspopup=\"dialog\" aria-expanded={open} aria-describedby={hintId} className={styles.trigger} onClick={() => (open ? close() : show())} onKeyDown={onTriggerKeyDown}>\n        <CalendarDays size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n        <span className={styles.srOnly}>{shown}</span>\n        <span className={styles.valueText} aria-hidden=\"true\">\n          <AnimatePresence mode=\"popLayout\" initial={false} custom={1}>\n            {value ? <motion.span key=\"value\" className={styles.value} custom={1} variants={reduce ? valueFade : valueRoll} initial=\"enter\" animate=\"center\" exit=\"exit\">\n              {parts.map((part, index) => <motion.span key={`${index}-${part.type}`} className={styles.part} layout=\"position\" layoutDependency={time} transition={partMotion}>\n                {part.type === \"literal\" ? <span className={styles.partValue}>{part.value}</span> : <AnimatePresence mode=\"popLayout\" initial={false} custom={direction}><motion.span key={part.value} className={styles.partValue} custom={direction} variants={reduce ? valueFade : valueRoll} initial=\"enter\" animate=\"center\" exit=\"exit\">{part.value}</motion.span></AnimatePresence>}\n              </motion.span>)}\n            </motion.span> : <motion.span key=\"placeholder\" className={styles.placeholder} custom={-1} variants={reduce ? valueFade : valueRoll} initial=\"enter\" animate=\"center\" exit=\"exit\">{placeholder}</motion.span>}\n          </AnimatePresence>\n        </span>\n        <ChevronDown className={styles.chevron} size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n      </button>\n      <AnimatePresence>\n        {open && <motion.div ref={popoverRef} className={styles.popover} role=\"dialog\" aria-label={`${label} calendar`} onKeyDown={(event) => { if (event.key === \"Escape\") { event.stopPropagation(); close(); } }}\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: -8, scale: .95 }}\n          animate={{ opacity: 1, y: 0, scale: 1, transition: reduce ? instant : { ...spring.snappy, opacity: { duration: duration.fast, ease: ease.enter } } }}\n          exit={{ opacity: 0, ...(reduce ? {} : { y: -6, scale: .97 }), transition: { duration: 0.14, ease: ease.standard } }}>\n          <Calendar value={value} onChange={selectDate} month={month} onMonthChange={setMonth} minDate={minDate} maxDate={maxDate} disabledDates={disabledDates} locale={locale} showToday={showToday} />\n          <div className={styles.footer}><button type=\"button\" onClick={() => selectDate(undefined)} disabled={!value}>Clear</button><span className={styles.status}><AnimatePresence initial={false}><motion.span key={time ?? \"none\"} variants={reduce ? valueFade : statusRise} initial=\"enter\" animate=\"center\" exit=\"exit\">{value ? `Selected ${formatter.format(value)}` : \"Choose a day\"}</motion.span></AnimatePresence></span></div>\n        </motion.div>}\n      </AnimatePresence>\n    </div>\n    {description && <span id={hintId} className={styles.description}>{description}</span>}\n  </div>;\n}\n"
    },
    {
      "path": "registry/components/date-picker/date-picker.module.css",
      "type": "registry:component",
      "target": "@components/xzero/date-picker/date-picker.module.css",
      "content": ".field { display: grid; gap: 7px; min-width: 0; }\n.label { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }\n.anchor { position: relative; min-width: 0; }\n/* The trigger anchors the calendar, so press feedback stays in color; it never scales. */\n.trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: 9px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: 0 12px; background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }\n@media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-muted); } }\n.trigger:active:not(:disabled), .trigger[aria-expanded=\"true\"] { border-color: var(--border-strong); background: var(--surface-muted); }\n.trigger:disabled { cursor: not-allowed; opacity: .5; }\n.trigger > svg:first-child { flex: 0 0 auto; color: var(--text-muted); }\n/* Old and new values cross in place; each part of a date is its own box so a wider day or month moves the rest along on a spring. */\n.valueText { position: relative; display: flex; flex: 1; min-width: 0; }\n.value, .part { position: relative; display: inline-flex; white-space: pre; }\n.value { font-variant-numeric: tabular-nums; }\n.partValue { display: inline-block; white-space: pre; }\n.placeholder { min-width: 0; overflow: hidden; color: var(--text-muted); text-overflow: ellipsis; white-space: nowrap; }\n.srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }\n.chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring); }\n.trigger[aria-expanded=\"true\"] .chevron { transform: rotate(180deg); }\n/* Grows out of the trigger's calendar icon and returns there on close. */\n.popover { position: absolute; z-index: 20; top: calc(100% + 8px); left: 0; width: min(320px, calc(100vw - 32px)); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-panel); padding: 16px; background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: 22px -8px; will-change: transform, opacity; }\n/* On phone widths the calendar matches the field instead of running past the screen edge, and a tighter inset keeps the month and its controls on one row. */\n@media (max-width: 360px) { .popover { right: 0; width: auto; padding: 12px; } }\n.footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 14px; border-top: 1px solid var(--border-subtle); padding-top: 11px; color: var(--text-muted); font-size: var(--text-xs); }\n/* Old and new status lines share one cell, pinned to the trailing edge so a shorter line never shifts. */\n.status { display: grid; min-width: 0; justify-items: end; }\n.status > span { grid-area: 1 / 1; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.footer button { border: 0; border-radius: var(--radius-control); padding: 4px 8px; background: transparent; color: var(--text-secondary); font: inherit; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }\n@media (hover: hover) and (pointer: fine) { .footer button:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); } }\n.footer button:active:not(:disabled) { transform: scale(.97); transition-duration: var(--duration-fast), var(--duration-fast), 100ms; transition-timing-function: var(--ease-standard); }\n.footer button:disabled { cursor: not-allowed; opacity: .4; }\n.description { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); transition: opacity var(--duration-fast) var(--ease-standard); }\n/* The open calendar covers the hint; fading it keeps its first letters from peeking past the popover's rounded corner. */\n.anchor:has(.popover) + .description { opacity: 0; }\n@media (prefers-reduced-motion: reduce) { .trigger, .chevron, .footer button, .description { transition: none; } .footer button:active:not(:disabled) { transform: none; } }\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/date-picker",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "date",
      "field"
    ],
    "docs": "https://ui.xzero.co.uk/components/date-picker",
    "markdown": "https://ui.xzero.co.uk/components/date-picker/markdown"
  }
}