{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-picker",
  "type": "registry:ui",
  "title": "Date Picker",
  "description": "A calendar date picker with an ARIA grid, sliding months, single or range selection and a typed ISO field with a native popover.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/date-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useIsPresent } from \"motion/react\";\n\n/* -------------------------------------------------------------------------- */\n/*  Dates as UTC day numbers: no time zones, no DST, deterministic on server   */\n/* -------------------------------------------------------------------------- */\n\nconst DAY_MS = 86_400_000;\n\nexport interface DateRange {\n  /** \"yyyy-mm-dd\" or null. */\n  start: string | null;\n  /** \"yyyy-mm-dd\" or null. */\n  end: string | null;\n}\n\nexport type WeekStart = \"sunday\" | \"monday\" | \"saturday\";\nconst WEEK_START_INDEX: Record<WeekStart, number> = { sunday: 0, monday: 1, saturday: 6 };\n\nconst mod = (n: number, m: number) => ((n % m) + m) % m;\n\n/** Parses \"yyyy-mm-dd\" (also \"/\" or \".\" separators). Returns a UTC day number or null. */\nexport function parseIsoDate(text: string): number | null {\n  const match = /^(\\d{4})[-/.](\\d{1,2})[-/.](\\d{1,2})$/.exec(text.trim());\n  if (!match) return null;\n  const [y, m, d] = [Number(match[1]), Number(match[2]) - 1, Number(match[3])];\n  const t = Date.UTC(y, m, d);\n  const date = new Date(t);\n  if (date.getUTCFullYear() !== y || date.getUTCMonth() !== m || date.getUTCDate() !== d) return null;\n  return t / DAY_MS;\n}\n\nconst toIso = (day: number) => new Date(day * DAY_MS).toISOString().slice(0, 10);\nconst weekdayOf = (day: number) => mod(day + 4, 7); // 1970-01-01 was a Thursday\nconst monthKeyOf = (day: number) => {\n  const d = new Date(day * DAY_MS);\n  return d.getUTCFullYear() * 12 + d.getUTCMonth();\n};\nconst monthStart = (key: number) => Date.UTC(Math.floor(key / 12), mod(key, 12), 1) / DAY_MS;\nconst monthEnd = (key: number) => monthStart(key + 1) - 1;\n\n/** Same day-of-month in another month, clamped to that month's length. */\nfunction addMonths(day: number, months: number) {\n  const d = new Date(day * DAY_MS);\n  const key = d.getUTCFullYear() * 12 + d.getUTCMonth() + months;\n  return Math.min(monthStart(key) + d.getUTCDate() - 1, monthEnd(key));\n}\n\nconst isoOrNull = (text: string | null | undefined) => (text ? parseIsoDate(text) : null);\n\n/* -------------------------------------------------------------------------- */\n/*  Browser-only values, kept out of server markup                             */\n/* -------------------------------------------------------------------------- */\n\nconst noopSubscribe = () => () => {};\nfunction localTodayIso() {\n  const now = new Date();\n  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n}\n/** The user's local \"today\" — null on the server and during hydration, so SSR stays deterministic. */\nfunction useTodayIso(override?: string) {\n  const client = React.useSyncExternalStore(noopSubscribe, localTodayIso, () => null);\n  return override ?? client;\n}\n\nfunction subscribeReducedMotion(cb: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", cb);\n  return () => mq.removeEventListener(\"change\", cb);\n}\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nfunction makeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {\n  try {\n    return new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" });\n  } catch {\n    return new Intl.DateTimeFormat(\"en-GB\", { ...options, timeZone: \"UTC\" });\n  }\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Props                                                                      */\n/* -------------------------------------------------------------------------- */\n\nexport interface DatePickerProps {\n  /** Pick one date, or a start and end date. */\n  mode?: \"single\" | \"range\";\n  /** A text field with a calendar popover, or the calendar on its own. */\n  variant?: \"popover\" | \"inline\";\n  /** Selected date in single mode, \"yyyy-mm-dd\" (controlled). */\n  value?: string | null;\n  /** Initial date in single mode (uncontrolled). */\n  defaultValue?: string | null;\n  /** Called with \"yyyy-mm-dd\" or null in single mode. */\n  onValueChange?: (value: string | null) => void;\n  /** Selected range in range mode (controlled). */\n  range?: DateRange;\n  /** Initial range in range mode (uncontrolled). */\n  defaultRange?: DateRange;\n  /** Called whenever the range changes (end is null while the second date is pending). */\n  onRangeChange?: (range: DateRange) => void;\n  /** Earliest selectable date, \"yyyy-mm-dd\". */\n  min?: string;\n  /** Latest selectable date, \"yyyy-mm-dd\". */\n  max?: string;\n  /** Marks extra dates as unavailable (weekends, booked days…). */\n  isDateDisabled?: (iso: string) => boolean;\n  /** First column of the week. */\n  weekStart?: WeekStart;\n  /** BCP 47 locale for month and weekday names. */\n  locale?: string;\n  /** Overrides \"today\" (\"yyyy-mm-dd\"). Without it, today is read on the client after hydration. */\n  today?: string;\n  /** Visible label of the field / calendar. */\n  label?: string;\n  /** Close the popover once a date (or the range end) is picked. */\n  closeOnSelect?: boolean;\n  /** Selection, today marker and focus colour. */\n  accent?: string;\n  disabled?: boolean;\n  /** Submits the ISO value with a form. In range mode two hidden inputs share the name (start, end). */\n  name?: string;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Month grid (one per visible month, slid in and out)                        */\n/* -------------------------------------------------------------------------- */\n\ninterface MonthGridProps {\n  monthKey: number;\n  weekStart: number;\n  tabStop: number;\n  renderDay: (day: number, tabStop: boolean) => React.ReactNode;\n  direction: number;\n  reduceMotion: boolean;\n}\n\nfunction MonthGrid({ monthKey, weekStart, tabStop, renderDay, direction, reduceMotion }: MonthGridProps) {\n  const present = useIsPresent();\n  const first = monthStart(monthKey);\n  const last = monthEnd(monthKey);\n  const gridStart = first - mod(weekdayOf(first) - weekStart, 7);\n  const offset = reduceMotion ? 0 : 28;\n\n  return (\n    <motion.div\n      role=\"rowgroup\"\n      aria-hidden={!present || undefined}\n      inert={!present || undefined}\n      className=\"absolute inset-x-0 top-0 flex flex-col\"\n      // Exiting months read the latest direction from AnimatePresence's `custom`.\n      custom={direction}\n      variants={{\n        enter: (d: number) => ({ x: d * offset, opacity: 0 }),\n        center: { x: 0, opacity: 1 },\n        exit: (d: number) => ({ x: -d * offset, opacity: 0 }),\n      }}\n      initial=\"enter\"\n      animate=\"center\"\n      exit=\"exit\"\n      transition={reduceMotion ? { duration: 0 } : { type: \"spring\", stiffness: 420, damping: 38, mass: 0.8 }}\n    >\n      {/* Always six weeks, so the height never jumps between months. */}\n      {Array.from({ length: 6 }, (_, week) => (\n        <div key={week} role=\"row\" className=\"flex\">\n          {Array.from({ length: 7 }, (_, i) => {\n            const day = gridStart + week * 7 + i;\n            if (day < first || day > last) return <div key={i} role=\"gridcell\" className=\"size-9 flex-1\" />;\n            return <React.Fragment key={i}>{renderDay(day, day === tabStop)}</React.Fragment>;\n          })}\n        </div>\n      ))}\n    </motion.div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*  Date picker                                                                */\n/* -------------------------------------------------------------------------- */\n\nconst RANGE_SPLIT = /^\\s*(\\d{4}[-/.]\\d{1,2}[-/.]\\d{1,2})\\s*(?:–|—|\\bto\\b|\\s-\\s|\\.\\.)\\s*(\\d{4}[-/.]\\d{1,2}[-/.]\\d{1,2})\\s*$/i;\n\n/**\n * A calendar date picker with an ARIA grid (arrows, Page Up / Down, Home / End), direction-aware\n * month transitions, single or range selection, min / max / disabled dates and locale-aware names.\n * The popover variant pairs a typed ISO field with a native popover and returns focus on close.\n * \"Today\" is never rendered on the server: pass `today`, or it is read after hydration.\n */\nexport function DatePicker({\n  mode = \"single\",\n  variant = \"inline\",\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  range: rangeProp,\n  defaultRange = { start: null, end: null },\n  onRangeChange,\n  min,\n  max,\n  isDateDisabled,\n  weekStart = \"monday\",\n  locale = \"en-GB\",\n  today: todayProp,\n  label = \"Date\",\n  closeOnSelect = true,\n  accent = \"#FF7448\",\n  disabled = false,\n  name,\n  className = \"\",\n  style,\n}: DatePickerProps) {\n  const id = React.useId();\n  const labelId = `${id}-label`;\n  const headingId = `${id}-heading`;\n  const popoverId = `${id}-popover`;\n  const errorId = `${id}-error`;\n  const reduceMotion = usePrefersReducedMotion();\n  const todayIso = useTodayIso(todayProp);\n  const today = isoOrNull(todayIso);\n  const startIndex = WEEK_START_INDEX[weekStart] ?? 1;\n  const isRange = mode === \"range\";\n  const isPopover = variant === \"popover\";\n\n  /* ---------------------------------- value --------------------------------- */\n\n  const [internalValue, setInternalValue] = React.useState<string | null>(defaultValue);\n  const [internalRange, setInternalRange] = React.useState<DateRange>(defaultRange);\n  const value = valueProp !== undefined ? valueProp : internalValue;\n  const currentRange = rangeProp ?? internalRange;\n  const selected = isoOrNull(value);\n  const rangeStart = isoOrNull(currentRange.start);\n  const rangeEnd = isoOrNull(currentRange.end);\n\n  const setValue = (next: string | null) => {\n    if (valueProp === undefined) setInternalValue(next);\n    onValueChange?.(next);\n  };\n  const setRange = (next: DateRange) => {\n    if (rangeProp === undefined) setInternalRange(next);\n    onRangeChange?.(next);\n  };\n\n  /* ---------------------------------- limits -------------------------------- */\n\n  const minDay = isoOrNull(min);\n  const maxDay = isoOrNull(max);\n  const isDisabled = (day: number) =>\n    (minDay !== null && day < minDay) || (maxDay !== null && day > maxDay) || Boolean(isDateDisabled?.(toIso(day)));\n  const clampDay = (day: number) => Math.min(maxDay ?? Infinity, Math.max(minDay ?? -Infinity, day));\n\n  /* ------------------------------ view and focus ----------------------------- */\n\n  const [viewMonth, setViewMonth] = React.useState<number | null>(null);\n  const [focusDay, setFocusDay] = React.useState<number | null>(null);\n  const [direction, setDirection] = React.useState(1);\n  const [hoverDay, setHoverDay] = React.useState<number | null>(null);\n  const [open, setOpen] = React.useState(false);\n  const [draft, setDraft] = React.useState<string | null>(null);\n  const [error, setError] = React.useState<string | null>(null);\n\n  const anchorDay = isRange ? (rangeStart ?? rangeEnd) : selected;\n  const visibleMonth = viewMonth ?? (anchorDay !== null ? monthKeyOf(anchorDay) : today !== null ? monthKeyOf(today) : null);\n\n  const gridRef = React.useRef<HTMLDivElement>(null);\n  const fieldRef = React.useRef<HTMLDivElement>(null);\n  const popoverRef = React.useRef<HTMLDivElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const pendingFocus = React.useRef(false);\n\n  // The roving tab stop: the focused day, else the selection, else today, else the first enabled day.\n  let tabStop = -1;\n  if (visibleMonth !== null) {\n    const first = monthStart(visibleMonth);\n    const last = monthEnd(visibleMonth);\n    const inMonth = (d: number | null): d is number => d !== null && d >= first && d <= last;\n    if (inMonth(focusDay)) tabStop = focusDay;\n    else if (inMonth(anchorDay)) tabStop = anchorDay;\n    else if (inMonth(today)) tabStop = today;\n    else {\n      tabStop = first;\n      for (let d = first; d <= last; d++) {\n        if (!isDisabled(d)) {\n          tabStop = d;\n          break;\n        }\n      }\n    }\n  }\n\n  // Move DOM focus after keyboard navigation re-renders the grid (possibly a new month).\n  React.useEffect(() => {\n    if (!pendingFocus.current || tabStop < 0) return;\n    pendingFocus.current = false;\n    gridRef.current?.querySelector<HTMLButtonElement>(`[data-day=\"${tabStop}\"]`)?.focus({ preventScroll: true });\n  });\n\n  const goToMonth = (key: number) => {\n    if (visibleMonth !== null && key !== visibleMonth) setDirection(key > visibleMonth ? 1 : -1);\n    setViewMonth(key);\n  };\n\n  const focusOn = (day: number) => {\n    const next = clampDay(day);\n    setFocusDay(next);\n    if (visibleMonth !== monthKeyOf(next)) goToMonth(monthKeyOf(next));\n    if (isRange && rangeStart !== null && rangeEnd === null) setHoverDay(next);\n    pendingFocus.current = true;\n  };\n\n  /* ---------------------------------- popover -------------------------------- */\n\n  const position = React.useCallback(() => {\n    const pop = popoverRef.current;\n    const field = fieldRef.current;\n    if (!pop || !field) return;\n    const rect = field.getBoundingClientRect();\n    const height = pop.offsetHeight;\n    const width = pop.offsetWidth;\n    const below = rect.bottom + 6;\n    const top = below + height > window.innerHeight - 8 && rect.top - height - 6 > 8 ? rect.top - height - 6 : below;\n    const left = Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8));\n    pop.style.top = `${top}px`;\n    pop.style.left = `${left}px`;\n  }, []);\n\n  React.useEffect(() => {\n    const pop = popoverRef.current;\n    if (!pop || !isPopover) return;\n    const onToggle = (event: Event) => {\n      const next = (event as ToggleEvent).newState === \"open\";\n      setOpen(next);\n      if (next) {\n        // Every opening starts at the selection (or today) with focus inside the grid.\n        setViewMonth(null);\n        setFocusDay(null);\n        setHoverDay(null);\n        pendingFocus.current = true;\n      } else if (pop.contains(document.activeElement) || document.activeElement === document.body) {\n        triggerRef.current?.focus({ preventScroll: true });\n      }\n    };\n    pop.addEventListener(\"toggle\", onToggle);\n    return () => pop.removeEventListener(\"toggle\", onToggle);\n  }, [isPopover]);\n\n  // Keep the popover attached to the field while it is open.\n  React.useEffect(() => {\n    if (!open) return;\n    position();\n    window.addEventListener(\"scroll\", position, { capture: true, passive: true });\n    window.addEventListener(\"resize\", position);\n    return () => {\n      window.removeEventListener(\"scroll\", position, { capture: true });\n      window.removeEventListener(\"resize\", position);\n    };\n  }, [open, position]);\n\n  const closePopover = () => {\n    const pop = popoverRef.current;\n    if (pop?.matches(\":popover-open\")) pop.hidePopover();\n  };\n  const openPopover = () => {\n    const pop = popoverRef.current;\n    if (pop && !pop.matches(\":popover-open\")) pop.showPopover();\n  };\n\n  /* --------------------------------- selection ------------------------------- */\n\n  const select = (day: number) => {\n    if (isDisabled(day) || disabled) return;\n    setFocusDay(day);\n    setDraft(null);\n    setError(null);\n    if (!isRange) {\n      setValue(toIso(day));\n      if (isPopover && closeOnSelect) closePopover();\n      return;\n    }\n    if (rangeStart === null || rangeEnd !== null) {\n      setRange({ start: toIso(day), end: null });\n      setHoverDay(day);\n      return;\n    }\n    // Second click: a date before the start swaps the ends.\n    const [a, b] = day < rangeStart ? [day, rangeStart] : [rangeStart, day];\n    setRange({ start: toIso(a), end: toIso(b) });\n    setHoverDay(null);\n    if (isPopover && closeOnSelect) closePopover();\n  };\n\n  const handleGridKey = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (tabStop < 0) return;\n    const from = tabStop;\n    const weekday = mod(weekdayOf(from) - startIndex, 7);\n    const moves: Record<string, () => number> = {\n      ArrowLeft: () => from - 1,\n      ArrowRight: () => from + 1,\n      ArrowUp: () => from - 7,\n      ArrowDown: () => from + 7,\n      PageUp: () => addMonths(from, event.shiftKey ? -12 : -1),\n      PageDown: () => addMonths(from, event.shiftKey ? 12 : 1),\n      Home: () => from - weekday,\n      End: () => from + (6 - weekday),\n    };\n    const move = moves[event.key];\n    if (!move) return;\n    event.preventDefault();\n    focusOn(move());\n  };\n\n  /* ---------------------------------- text field ----------------------------- */\n\n  const fieldText = isRange\n    ? currentRange.start\n      ? `${currentRange.start} – ${currentRange.end ?? \"\"}`.trim()\n      : \"\"\n    : (value ?? \"\");\n\n  const commitText = (text: string) => {\n    const trimmed = text.trim();\n    const fail = (message: string) => {\n      setError(message);\n      return false;\n    };\n    if (!trimmed) {\n      if (isRange) setRange({ start: null, end: null });\n      else setValue(null);\n      setDraft(null);\n      setError(null);\n      return true;\n    }\n    let days: number[];\n    if (isRange) {\n      const match = RANGE_SPLIT.exec(trimmed);\n      const a = match ? parseIsoDate(match[1]) : null;\n      const b = match ? parseIsoDate(match[2]) : null;\n      if (a === null || b === null) return fail(\"Use yyyy-mm-dd – yyyy-mm-dd\");\n      days = a <= b ? [a, b] : [b, a];\n    } else {\n      const day = parseIsoDate(trimmed);\n      if (day === null) return fail(\"Use yyyy-mm-dd\");\n      days = [day];\n    }\n    if (days.some(isDisabled)) return fail(\"That date is not available\");\n    if (isRange) setRange({ start: toIso(days[0]), end: toIso(days[1]) });\n    else setValue(toIso(days[0]));\n    setViewMonth(monthKeyOf(days[0]));\n    setDraft(null);\n    setError(null);\n    return true;\n  };\n\n  /* --------------------------------- rendering ------------------------------- */\n\n  const formats = React.useMemo(\n    () => ({\n      heading: makeFormatter(locale, { month: \"long\", year: \"numeric\" }),\n      full: makeFormatter(locale, { weekday: \"long\", day: \"numeric\", month: \"long\", year: \"numeric\" }),\n      weekdayShort: makeFormatter(locale, { weekday: \"short\" }),\n      weekdayLong: makeFormatter(locale, { weekday: \"long\" }),\n    }),\n    [locale],\n  );\n  const SUNDAY = Date.UTC(2023, 0, 1) / DAY_MS;\n  const weekdays = Array.from({ length: 7 }, (_, i) => new Date((SUNDAY + mod(startIndex + i, 7)) * DAY_MS));\n\n  // Range band, including the live preview while the end date is still being chosen.\n  const previewing = isRange && rangeStart !== null && rangeEnd === null && hoverDay !== null;\n  const bandFrom = rangeEnd !== null && rangeStart !== null ? rangeStart : previewing ? Math.min(rangeStart, hoverDay) : null;\n  const bandTo = rangeEnd !== null && rangeStart !== null ? rangeEnd : previewing ? Math.max(rangeStart, hoverDay) : null;\n\n  const renderDay = (day: number, isTabStop: boolean) => {\n    const dayDisabled = isDisabled(day);\n    const isToday = day === today;\n    const endpoint = isRange ? day === rangeStart || day === rangeEnd : day === selected;\n    const inBand = bandFrom !== null && bandTo !== null && day >= bandFrom && day <= bandTo && bandFrom !== bandTo;\n    const ariaSelected = endpoint || (inBand && rangeEnd !== null);\n    const role = isRange && day === rangeStart ? \", range start\" : isRange && day === rangeEnd ? \", range end\" : \"\";\n    return (\n      <div role=\"gridcell\" aria-selected={ariaSelected} className=\"relative flex size-9 flex-1 items-center justify-center\">\n        {inBand && (\n          <span\n            aria-hidden=\"true\"\n            className={`absolute inset-y-1 ${previewing ? \"bg-[color-mix(in_oklab,var(--dp-accent)_10%,transparent)]\" : \"bg-[color-mix(in_oklab,var(--dp-accent)_20%,transparent)]\"}`}\n            style={{ left: day === bandFrom ? \"50%\" : 0, right: day === bandTo ? \"50%\" : 0 }}\n          />\n        )}\n        <button\n          type=\"button\"\n          data-day={day}\n          tabIndex={isTabStop ? 0 : -1}\n          aria-label={`${formats.full.format(new Date(day * DAY_MS))}${role}${dayDisabled ? \", unavailable\" : \"\"}`}\n          aria-current={isToday ? \"date\" : undefined}\n          aria-disabled={dayDisabled || disabled || undefined}\n          onClick={() => select(day)}\n          onFocus={() => {\n            setFocusDay(day);\n            if (isRange && rangeStart !== null && rangeEnd === null) setHoverDay(day);\n          }}\n          onPointerEnter={(event) => {\n            if (event.pointerType === \"mouse\" && isRange && rangeStart !== null && rangeEnd === null) setHoverDay(day);\n          }}\n          className={`relative inline-flex size-8 items-center justify-center rounded-full font-mono text-xs tabular-nums outline-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-[var(--dp-accent)] focus-visible:ring-offset-1 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-offset-zinc-900 ${\n            endpoint\n              ? \"bg-[var(--dp-accent)] font-medium text-zinc-950\"\n              : dayDisabled\n                ? \"cursor-not-allowed text-zinc-400 line-through decoration-zinc-400/60 dark:text-zinc-600 dark:decoration-zinc-600/60\"\n                : \"text-zinc-800 hover:bg-zinc-900/[0.06] dark:text-zinc-200 dark:hover:bg-white/[0.08]\"\n          }`}\n        >\n          {new Date(day * DAY_MS).getUTCDate()}\n          {isToday && (\n            <span aria-hidden=\"true\" className={`absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full ${endpoint ? \"bg-zinc-950\" : \"bg-[var(--dp-accent)]\"}`} />\n          )}\n        </button>\n      </div>\n    );\n  };\n\n  const prevDisabled = visibleMonth === null || (minDay !== null && monthEnd(visibleMonth - 1) < minDay);\n  const nextDisabled = visibleMonth === null || (maxDay !== null && monthStart(visibleMonth + 1) > maxDay);\n  const navButton =\n    \"inline-flex size-8 items-center justify-center rounded-md text-zinc-600 outline-none transition-colors hover:bg-zinc-900/[0.06] focus-visible:ring-2 focus-visible:ring-[var(--dp-accent)] disabled:pointer-events-none disabled:opacity-30 dark:text-zinc-300 dark:hover:bg-white/[0.08]\";\n  const footerButton =\n    \"rounded-md px-2 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-zinc-500 outline-none transition-colors hover:bg-zinc-900/[0.06] hover:text-zinc-900 focus-visible:ring-2 focus-visible:ring-[var(--dp-accent)] disabled:pointer-events-none disabled:opacity-40 dark:text-zinc-400 dark:hover:bg-white/[0.08] dark:hover:text-zinc-100\";\n  const hasSelection = isRange ? rangeStart !== null : selected !== null;\n\n  const calendar = (\n    <div className=\"w-[17.5rem] p-3\">\n      <div className=\"mb-2 flex items-center justify-between gap-2\">\n        <button type=\"button\" className={navButton} aria-label=\"Previous month\" disabled={prevDisabled} onClick={() => visibleMonth !== null && goToMonth(visibleMonth - 1)}>\n          <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" aria-hidden=\"true\">\n            <path d=\"M10 3.5 5.5 8l4.5 4.5\" />\n          </svg>\n        </button>\n        <h2 id={headingId} aria-live=\"polite\" className=\"min-w-0 truncate text-sm font-medium capitalize tracking-tight text-zinc-900 dark:text-zinc-100\">\n          {visibleMonth !== null ? formats.heading.format(new Date(monthStart(visibleMonth) * DAY_MS)) : \" \"}\n        </h2>\n        <button type=\"button\" className={navButton} aria-label=\"Next month\" disabled={nextDisabled} onClick={() => visibleMonth !== null && goToMonth(visibleMonth + 1)}>\n          <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" aria-hidden=\"true\">\n            <path d=\"M6 3.5 10.5 8 6 12.5\" />\n          </svg>\n        </button>\n      </div>\n\n      <div\n        ref={gridRef}\n        role=\"grid\"\n        aria-labelledby={headingId}\n        aria-multiselectable={isRange || undefined}\n        aria-disabled={disabled || undefined}\n        onKeyDown={handleGridKey}\n        onPointerLeave={() => previewing && setHoverDay(null)}\n      >\n        <div role=\"rowgroup\">\n          <div role=\"row\" className=\"flex\">\n            {weekdays.map((d) => (\n              <div\n                key={d.getTime()}\n                role=\"columnheader\"\n                aria-label={formats.weekdayLong.format(d)}\n                className=\"flex h-7 flex-1 items-center justify-center font-mono text-[10px] uppercase tracking-[0.08em] text-zinc-500 dark:text-zinc-400\"\n              >\n                <span aria-hidden=\"true\">{formats.weekdayShort.format(d).replace(/\\.$/, \"\").slice(0, 2)}</span>\n              </div>\n            ))}\n          </div>\n        </div>\n        {/* Fixed height (6 × 36px) so sliding months overlap instead of pushing the footer. */}\n        <div className=\"relative h-[13.5rem] overflow-hidden\">\n          {visibleMonth !== null ? (\n            <AnimatePresence initial={false} custom={direction}>\n              <MonthGrid\n                key={visibleMonth}\n                monthKey={visibleMonth}\n                weekStart={startIndex}\n                tabStop={tabStop}\n                renderDay={renderDay}\n                direction={direction}\n                reduceMotion={reduceMotion}\n              />\n            </AnimatePresence>\n          ) : (\n            <div role=\"rowgroup\" aria-hidden=\"true\" className=\"grid h-full grid-cols-7 place-items-center\">\n              {Array.from({ length: 35 }, (_, i) => (\n                <span key={i} className=\"size-1 rounded-full bg-zinc-900/10 dark:bg-white/10\" />\n              ))}\n            </div>\n          )}\n        </div>\n      </div>\n\n      <div className=\"mt-2 flex items-center justify-between border-t border-zinc-900/10 pt-2 dark:border-white/10\">\n        <button\n          type=\"button\"\n          className={footerButton}\n          disabled={today === null || isDisabled(today)}\n          onClick={() => today !== null && focusOn(today)}\n        >\n          Today\n        </button>\n        <span aria-hidden=\"true\" className=\"font-mono text-[10px] tracking-[0.06em] text-zinc-400 dark:text-zinc-500\">\n          {isRange && rangeStart !== null && rangeEnd === null ? \"Pick an end date\" : \"\"}\n        </span>\n        <button\n          type=\"button\"\n          className={footerButton}\n          disabled={!hasSelection}\n          onClick={() => {\n            if (isRange) setRange({ start: null, end: null });\n            else setValue(null);\n            setHoverDay(null);\n            setDraft(null);\n            setError(null);\n          }}\n        >\n          Clear\n        </button>\n      </div>\n    </div>\n  );\n\n  const hidden =\n    name === undefined ? null : isRange ? (\n      <>\n        <input type=\"hidden\" name={name} value={currentRange.start ?? \"\"} />\n        <input type=\"hidden\" name={name} value={currentRange.end ?? \"\"} />\n      </>\n    ) : (\n      <input type=\"hidden\" name={name} value={value ?? \"\"} />\n    );\n\n  const rootStyle = { [\"--dp-accent\" as string]: accent, ...style };\n\n  if (!isPopover) {\n    return (\n      <div\n        role=\"group\"\n        aria-labelledby={labelId}\n        className={`inline-flex flex-col gap-2 ${disabled ? \"pointer-events-none opacity-50\" : \"\"} ${className}`}\n        style={rootStyle}\n      >\n        <span id={labelId} className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n          {label}\n        </span>\n        <div className=\"rounded-xl border border-zinc-900/10 bg-white dark:border-white/10 dark:bg-zinc-900\">{calendar}</div>\n        {hidden}\n      </div>\n    );\n  }\n\n  return (\n    <div className={`inline-flex w-full max-w-[17.5rem] flex-col gap-1.5 ${className}`} style={rootStyle}>\n      <label id={labelId} htmlFor={`${id}-input`} className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n        {label}\n      </label>\n      <div\n        ref={fieldRef}\n        className=\"flex h-10 items-stretch rounded-lg border border-zinc-900/15 bg-white transition-colors focus-within:border-[var(--dp-accent)] dark:border-white/15 dark:bg-white/[0.04]\"\n      >\n        <input\n          id={`${id}-input`}\n          type=\"text\"\n          inputMode=\"numeric\"\n          autoComplete=\"off\"\n          spellCheck={false}\n          disabled={disabled}\n          placeholder={isRange ? \"yyyy-mm-dd – yyyy-mm-dd\" : \"yyyy-mm-dd\"}\n          value={draft ?? fieldText}\n          aria-invalid={error ? true : undefined}\n          aria-describedby={errorId}\n          onChange={(event) => {\n            setDraft(event.target.value);\n            setError(null);\n          }}\n          onBlur={() => {\n            if (draft !== null) commitText(draft);\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\" && draft !== null) {\n              event.preventDefault();\n              commitText(draft);\n            } else if (event.key === \"ArrowDown\" && event.altKey) {\n              event.preventDefault();\n              openPopover();\n            }\n          }}\n          className=\"min-w-0 flex-1 bg-transparent px-3 font-mono text-[13px] tabular-nums text-zinc-900 outline-none placeholder:text-zinc-400 aria-[invalid]:text-red-600 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:aria-[invalid]:text-red-400\"\n        />\n        <button\n          ref={triggerRef}\n          type=\"button\"\n          disabled={disabled}\n          popoverTarget={popoverId}\n          aria-label={`Choose ${label.toLowerCase()}`}\n          aria-haspopup=\"dialog\"\n          aria-expanded={open}\n          aria-controls={popoverId}\n          className=\"inline-flex w-10 shrink-0 items-center justify-center border-l border-zinc-900/10 text-zinc-500 outline-none transition-colors hover:text-zinc-900 focus-visible:bg-zinc-900/[0.05] focus-visible:text-[var(--dp-accent)] dark:border-white/10 dark:text-zinc-400 dark:hover:text-zinc-100 dark:focus-visible:bg-white/[0.06]\"\n        >\n          <svg viewBox=\"0 0 16 16\" className=\"size-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.3\" aria-hidden=\"true\">\n            <rect x=\"2.5\" y=\"3.5\" width=\"11\" height=\"10\" rx=\"1.5\" />\n            <path d=\"M2.5 6.5h11M5.5 2v3M10.5 2v3\" strokeLinecap=\"round\" />\n          </svg>\n        </button>\n      </div>\n      <p id={errorId} aria-live=\"polite\" className=\"min-h-4 font-mono text-[10px] tracking-[0.04em] text-red-600 dark:text-red-400\">\n        {error ?? \"\"}\n      </p>\n\n      <div\n        ref={popoverRef}\n        id={popoverId}\n        popover=\"auto\"\n        role=\"dialog\"\n        aria-labelledby={labelId}\n        className=\"fixed inset-auto m-0 overflow-visible rounded-xl border border-zinc-900/10 bg-white p-0 text-zinc-900 shadow-[0_24px_48px_-24px_rgb(0_0_0/0.35)] dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100\"\n        style={rootStyle}\n      >\n        {open ? calendar : null}\n      </div>\n      {hidden}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/date-picker"
}
