{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "title": "Date range picker",
  "description": "A button that opens two months side by side, with quick ranges such as Last 7 days, a switch to compare with the previous period of equal length, and Cancel and Apply. DateRangePanel shows the same panel inline.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@manniche/switch"
  ],
  "files": [
    {
      "path": "registry/manniche/date-range-picker/date-range-picker.tsx",
      "content": "// DateRangePicker: a button that shows the chosen range and opens a panel to change it. The panel is exported on its\n// own too (DateRangePanel), to sit inline in a page or a tile. On the left, quick ranges (Today, Last 7 days, …) with\n// a check on the active one; on the right, two months side by side (one when the panel is narrower than about 640 px).\n// Picking days by hand switches the quick range to Custom. The footer has a switch that compares with the period of\n// equal length just before, and shows that period, then Cancel and Apply. Nothing is applied until Apply.\n//\n// Dates are plain local days: every helper below works on year, month and day (new Date(y, m, d)), never on\n// milliseconds, so a range never shifts by a day across time zones or daylight saving. Pass and get back Date objects\n// at local midnight. Month and weekday names come from Intl in the given `locale`.\n//\n// Keyboard: the days are one grid with a single tab stop (roving tabindex), across both months. Arrows move a day or a\n// week, Home and End go to the start and end of the week, PageUp and PageDown a month (with Shift a year), Enter or\n// Space picks. The quick ranges are a radio group (arrows move and pick). In the popover, Tab stays inside, and Esc or\n// a click outside closes it without applying and puts focus back on the button.\n//\n// Screen readers: each month is a grid labelled by its name; each day is a button named by its full date, plus\n// \"start of range\", \"end of range\", \"in range\", \"today\" and so on, and its cell carries aria-selected. A polite status\n// says what was picked and how many days it covers.\n//\n// Motion: the popover rises from its button in 160 ms when opened by pointer; from the keyboard and under reduced\n// motion it is instant. Days fade their colour in 150 ms. Nothing else moves.\nimport { CalendarDays, Check, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n} from 'react'\nimport { cn } from '@/lib/utils'\nimport { useReducedMotion } from '@/registry/manniche/hooks/use-reduced-motion'\nimport { Switch } from '@/registry/manniche/switch/switch'\nimport {\n  addDays,\n  addMonths,\n  before,\n  dayOf,\n  daysBetween,\n  endOfMonth,\n  iso,\n  keyOf,\n  monthsBetween,\n  ordered,\n  presetRange,\n  previousPeriod,\n  sameDay,\n  startOfMonth,\n  startOfWeek,\n  type DateRange,\n  type DateRangePreset,\n} from '@/registry/manniche/date-range-picker/dates'\n\nexport type { DateRange, DateRangePreset }\n\n// ─── Types ──────────────────────────────────────────────────────────────────────────────────────────────────────────\n\n/** What Apply hands back. */\nexport type DateRangeApplyDetail = {\n  /** The applied range. */\n  range: DateRange\n  /** The quick range that was active, or \"custom\". */\n  preset: DateRangePreset\n  /** Whether the compare switch was on. */\n  compare: boolean\n  /** The period of equal length just before the range, whether or not compare is on. */\n  previous: DateRange\n}\n\nexport type DateRangePickerLabels = {\n  /** Names the trigger button, read before its text. Default \"Date range\". */\n  trigger: string\n  /** The trigger text when nothing is chosen. Default \"Pick a date range\". */\n  placeholder: string\n  /** Names the popover. Default \"Choose a date range\". */\n  dialog: string\n  /** Names the list of quick ranges. Default \"Quick ranges\". */\n  presets: string\n  today: string\n  yesterday: string\n  last7: string\n  last30: string\n  thisMonth: string\n  lastMonth: string\n  custom: string\n  /** The month arrows. Defaults \"Previous month\" and \"Next month\". */\n  previousMonth: string\n  nextMonth: string\n  /** The switch in the footer. Default \"Compare to previous period\". */\n  compare: string\n  /** Before the compared period on the trigger. Default \"vs\". */\n  versus: string\n  cancel: string\n  apply: string\n  /** Added to a day's name for screen readers. */\n  rangeStart: string\n  rangeEnd: string\n  inRange: string\n  inComparison: string\n  isToday: string\n  unavailable: string\n  /** Spoken after the first pick; `{date}` is the day. Default \"Start {date}. Now pick the end date.\" */\n  pickEnd: string\n  /** Spoken when a range is complete. `{range}` and `{count}`. Defaults \"{range}, {count} days.\" and \"{range}, 1 day.\" */\n  picked: string\n  pickedOne: string\n}\n\nconst LABELS: DateRangePickerLabels = {\n  trigger: 'Date range',\n  placeholder: 'Pick a date range',\n  dialog: 'Choose a date range',\n  presets: 'Quick ranges',\n  today: 'Today',\n  yesterday: 'Yesterday',\n  last7: 'Last 7 days',\n  last30: 'Last 30 days',\n  thisMonth: 'This month',\n  lastMonth: 'Last month',\n  custom: 'Custom range',\n  previousMonth: 'Previous month',\n  nextMonth: 'Next month',\n  compare: 'Compare to previous period',\n  versus: 'vs',\n  cancel: 'Cancel',\n  apply: 'Apply',\n  rangeStart: 'start of range',\n  rangeEnd: 'end of range',\n  inRange: 'in range',\n  inComparison: 'in comparison period',\n  isToday: 'today',\n  unavailable: 'unavailable',\n  pickEnd: 'Start {date}. Now pick the end date.',\n  picked: '{range}, {count} days.',\n  pickedOne: '{range}, 1 day.',\n}\n\nconst ALL_PRESETS: DateRangePreset[] = ['today', 'yesterday', 'last7', 'last30', 'thisMonth', 'lastMonth', 'custom']\n\ntype Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6\n\nexport type DateRangePanelProps = {\n  /** The applied range (controlled). `null` for none. Leave it out to let the panel keep track. */\n  value?: DateRange | null\n  /** The range applied at the start when not controlled. */\n  defaultValue?: DateRange | null\n  /** Called with the range when Apply is pressed. */\n  onChange?: (range: DateRange) => void\n  /** Called on Apply with the range, the active quick range, the compare state and the previous period. */\n  onApply?: (detail: DateRangeApplyDetail) => void\n  /** Called on Cancel. The panel goes back to the applied range. */\n  onCancel?: () => void\n  /** Whether comparing to the previous period is on (controlled). Changes are handed back on Apply. */\n  compare?: boolean\n  /** Whether compare starts on when not controlled. Default false. */\n  defaultCompare?: boolean\n  /** Called with the new compare state on Apply, when it changed. */\n  onCompareChange?: (compare: boolean) => void\n  /** Which quick ranges to show, in order. Default all seven. Leave \"custom\" out to hide it. */\n  presets?: DateRangePreset[]\n  /** The first day of the week, 0 for Sunday to 6 for Saturday. Default 1 (Monday). */\n  weekStartsOn?: Weekday\n  /** BCP 47 locale for month and weekday names and the range text, such as \"de\" or \"en-GB\". Default \"en\". */\n  locale?: string\n  /** The earliest day that can be picked. */\n  min?: Date\n  /** The last day that can be picked, such as today for data that only goes back. */\n  max?: Date\n  /** Return true for days that cannot be picked. A range may still run across them. */\n  isDateDisabled?: (date: Date) => boolean\n  /** The day the quick ranges count from. Default the real today; pass one to pin it in tests and demos. */\n  today?: Date\n  /** Show one or two months. Default: two, or one when the panel is narrower than about 640 px. */\n  months?: 1 | 2\n  /** Text for other languages. Missing keys fall back to English. */\n  labels?: Partial<DateRangePickerLabels>\n  /** Classes for the outer panel. */\n  className?: string\n}\n\nexport type DateRangePickerProps = DateRangePanelProps & {\n  /** Whether the popover is open (controlled). */\n  open?: boolean\n  /** Whether the popover starts open when not controlled. */\n  defaultOpen?: boolean\n  /** Called with the new open state. */\n  onOpenChange?: (open: boolean) => void\n  /** Which edge of the button the popover lines up with. Default \"start\". */\n  align?: 'start' | 'end'\n  /** Classes for the trigger button. */\n  className?: string\n  /** Classes for the popover panel. */\n  panelClassName?: string\n}\n\nconst sameRange = (a: DateRange | null | undefined, b: DateRange | null | undefined) =>\n  !!a && !!b && sameDay(a.start, b.start) && sameDay(a.end, b.end)\n\nfunction matchPreset(range: DateRange | null | undefined, today: Date, presets: DateRangePreset[]): DateRangePreset | null {\n  if (!range) return null\n  return presets.find((p) => p !== 'custom' && sameRange(presetRange(p, today), range)) ?? (presets.includes('custom') ? 'custom' : null)\n}\n\nconst fill = (template: string, values: Record<string, string | number>) =>\n  template.replace(/\\{(\\w+)\\}/g, (m, k: string) => (k in values ? String(values[k]) : m))\n\n// Intl formatters are slow to build, so each locale gets one set.\nconst formatCache = new Map<string, ReturnType<typeof makeFormats>>()\nfunction makeFormats(locale: string) {\n  const f = (o: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(locale, o)\n  return {\n    month: f({ month: 'long', year: 'numeric' }),\n    weekdayShort: f({ weekday: 'short' }),\n    weekdayLong: f({ weekday: 'long' }),\n    day: f({ day: 'numeric' }),\n    full: f({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }),\n    range: f({ day: 'numeric', month: 'short', year: 'numeric' }),\n    rangeShort: f({ day: 'numeric', month: 'short' }),\n  }\n}\nfunction formatsFor(locale: string) {\n  let found = formatCache.get(locale)\n  if (!found) {\n    found = makeFormats(locale)\n    formatCache.set(locale, found)\n  }\n  return found\n}\n/** \"8 – 14 Oct 2026\" in the locale's own shape. Drops the year when the whole range is in `thisYear`. */\nfunction rangeText(locale: string, r: DateRange, thisYear?: number) {\n  const fm = formatsFor(locale)\n  const short = thisYear !== undefined && r.start.getFullYear() === thisYear && r.end.getFullYear() === thisYear\n  return (short ? fm.rangeShort : fm.range).formatRange(r.start, r.end)\n}\n\nfunction useControllable<T>(value: T | undefined, initial: T) {\n  const [own, setOwn] = useState(initial)\n  return [value !== undefined ? value : own, setOwn] as const\n}\n\n// The panel's widths: a column of quick ranges (176) beside two 280 px months, or the quick ranges on top.\nconst SIDE_MIN = 792\nconst TWO_MIN = 620\ntype Layout = { months: 1 | 2; side: boolean }\nconst layoutFor = (width: number): Layout => ({ months: width >= TWO_MIN ? 2 : 1, side: width >= SIDE_MIN })\n\nconst BAND = 'bg-[color-mix(in_oklab,var(--primary)_15%,transparent)]'\nconst BAND_PREVIEW = 'bg-[color-mix(in_oklab,var(--primary)_8%,transparent)]'\nconst TILE_SHADOW = 'shadow-[0_0_0_1px_color-mix(in_oklab,var(--foreground)_9%,transparent),0_1px_2px_rgba(0,0,0,0.03)]'\nconst FOCUS = 'outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'\n\n// ─── Panel ────────────────────────────────────────────────────────────────────────────────────────────────────────\n\ntype InternalPanelProps = DateRangePanelProps & {\n  /** Set by the popover, which knows its own width; inline panels measure themselves. */\n  layout?: Layout\n}\n\n/** The picker on its own: quick ranges, months, compare switch, Cancel and Apply. Fills the width it is given. */\nexport function DateRangePanel(props: DateRangePanelProps) {\n  return <Panel {...props} />\n}\n\nfunction Panel({\n  value,\n  defaultValue = null,\n  onChange,\n  onApply,\n  onCancel,\n  compare,\n  defaultCompare = false,\n  onCompareChange,\n  presets = ALL_PRESETS,\n  weekStartsOn = 1,\n  locale = 'en',\n  min,\n  max,\n  isDateDisabled,\n  today: todayProp,\n  months: monthsProp,\n  labels: labelsProp,\n  className,\n  layout: fixedLayout,\n}: InternalPanelProps) {\n  const L = useMemo(() => ({ ...LABELS, ...labelsProp }), [labelsProp])\n  const fm = formatsFor(locale)\n  // The clock is read once, when the panel mounts, so a render never sees a different day.\n  const [mountedAt] = useState(() => new Date())\n  const today = dayOf(todayProp ?? mountedAt)\n  const minDay = min ? dayOf(min) : null\n  const maxDay = max ? dayOf(max) : null\n\n  const [applied, setApplied] = useControllable<DateRange | null>(value, defaultValue)\n  const [appliedCompare, setAppliedCompare] = useControllable<boolean>(compare, defaultCompare)\n\n  // Draft: what the panel shows until Apply.\n  const [start, setStart] = useState<Date | null>(applied?.start ?? null)\n  const [end, setEnd] = useState<Date | null>(applied?.end ?? null)\n  const [preset, setPreset] = useState<DateRangePreset | null>(() => matchPreset(applied, today, presets))\n  const [compareOn, setCompareOn] = useState(appliedCompare)\n  const [preview, setPreview] = useState<Date | null>(null)\n  const [status, setStatus] = useState('')\n\n  const isDisabled = (d: Date) => (!!minDay && before(d, minDay)) || (!!maxDay && before(maxDay, d)) || !!isDateDisabled?.(d)\n  const clamp = (d: Date) => (minDay && before(d, minDay) ? minDay : maxDay && before(maxDay, d) ? maxDay : d)\n\n  // Width: the popover passes its layout; inline, the panel measures the room it has.\n  const root = useRef<HTMLDivElement>(null)\n  const [measured, setMeasured] = useState<Layout>({ months: 2, side: true })\n  useLayoutEffect(() => {\n    if (fixedLayout) return\n    const el = root.current\n    if (!el) return\n    const update = () => {\n      const next = layoutFor(el.getBoundingClientRect().width)\n      setMeasured((m) => (m.months === next.months && m.side === next.side ? m : next))\n    }\n    update()\n    const ro = new ResizeObserver(update)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [fixedLayout])\n  const layout = fixedLayout ?? measured\n  const monthCount = monthsProp ?? layout.months\n\n  // The day with the grid's one tab stop, and the first month on show.\n  const [focused, setFocused] = useState<Date>(() => clamp(applied?.end ?? today))\n  const [view, setView] = useState<Date>(() => {\n    const anchor = clamp(applied?.end ?? today)\n    const v = addMonths(startOfMonth(anchor), -(monthCount - 1))\n    return applied && before(v, startOfMonth(applied.start)) && monthsBetween(applied.start, anchor) < monthCount ? startOfMonth(applied.start) : v\n  })\n\n  /** The first month to show so that `d` is on screen, moving as little as possible. */\n  const viewFor = (v: Date, d: Date, count = monthCount) => {\n    const m = startOfMonth(d)\n    if (before(m, v)) return m\n    const last = addMonths(v, count - 1)\n    return before(last, m) ? addMonths(m, -(count - 1)) : v\n  }\n  const showMonth = (d: Date) => setView((v) => viewFor(v, d))\n\n  /** Puts the draft back to the applied value: on Cancel, and when a new value comes in from outside. */\n  const reset = () => {\n    setStart(applied?.start ?? null)\n    setEnd(applied?.end ?? null)\n    setPreset(matchPreset(applied, today, presets))\n    setCompareOn(appliedCompare)\n    setPreview(null)\n    if (applied) {\n      setFocused(clamp(applied.end))\n      showMonth(clamp(applied.end))\n    }\n  }\n\n  // Changes from outside are handled while rendering, not in an effect, so there is no extra paint:\n  // a new applied value puts the draft back, and going from two months to one keeps the focused day on show.\n  const appliedKey = applied ? `${keyOf(applied.start)}-${keyOf(applied.end)}` : ''\n  const [seen, setSeen] = useState({ appliedKey, appliedCompare, monthCount })\n  if (seen.appliedKey !== appliedKey || seen.appliedCompare !== appliedCompare || seen.monthCount !== monthCount) {\n    setSeen({ appliedKey, appliedCompare, monthCount })\n    if (seen.appliedKey !== appliedKey || seen.appliedCompare !== appliedCompare) reset()\n    else showMonth(focused)\n  }\n\n  const picking = !!start && !end\n  const draft: DateRange | null = start && end ? { start, end } : null\n  const previous = draft ? previousPeriod(draft) : null\n\n  // Where the band runs: the draft, or while picking the end, from the start to the day under the pointer or focus.\n  const shown: Shown | null = draft\n    ? { ...draft, preview: false, anchor: null }\n    : start\n      ? { ...ordered(start, preview ?? start), preview: true, anchor: start }\n      : null\n\n  const announce = (r: DateRange) => {\n    const count = daysBetween(r.start, r.end) + 1\n    setStatus(fill(count === 1 ? L.pickedOne : L.picked, { range: rangeText(locale, r), count }))\n  }\n\n  const pick = (d: Date) => {\n    if (isDisabled(d)) return\n    setFocused(d)\n    if (picking && start) {\n      const r = ordered(start, d)\n      setStart(r.start)\n      setEnd(r.end)\n      setPreview(null)\n      announce(r)\n    } else {\n      setStart(d)\n      setEnd(null)\n      setStatus(fill(L.pickEnd, { date: fm.full.format(d) }))\n    }\n    if (presets.includes('custom')) setPreset('custom')\n    else setPreset(null)\n  }\n\n  const presetAllowed = (p: DateRangePreset) => {\n    if (p === 'custom') return true\n    const r = presetRange(p, today)\n    return !!r && !isDisabled(r.start) && !isDisabled(r.end)\n  }\n\n  const choosePreset = (p: DateRangePreset) => {\n    setPreset(p)\n    if (p === 'custom') return\n    const r = presetRange(p, today)\n    if (!r) return\n    setStart(r.start)\n    setEnd(r.end)\n    setPreview(null)\n    setFocused(r.end)\n    // Show the whole range when it fits, with its end in the last month on show.\n    setView(() => {\n      const v = addMonths(startOfMonth(r.end), -(monthCount - 1))\n      return before(v, startOfMonth(r.start)) ? startOfMonth(r.start) : v\n    })\n    announce(r)\n  }\n\n  // Focus follows the roving day after a key moves it.\n  const grids = useRef<HTMLDivElement>(null)\n  const moveFocus = useRef(false)\n  useEffect(() => {\n    if (!moveFocus.current) return\n    moveFocus.current = false\n    grids.current?.querySelector<HTMLButtonElement>(`[data-date=\"${iso(focused)}\"]`)?.focus()\n  }, [focused, view])\n\n  const onGridKey = (e: ReactKeyboardEvent) => {\n    let next: Date | null = null\n    const big = e.shiftKey ? 12 : 1\n    switch (e.key) {\n      case 'ArrowLeft':\n        next = addDays(focused, -1)\n        break\n      case 'ArrowRight':\n        next = addDays(focused, 1)\n        break\n      case 'ArrowUp':\n        next = addDays(focused, -7)\n        break\n      case 'ArrowDown':\n        next = addDays(focused, 7)\n        break\n      case 'Home':\n        next = startOfWeek(focused, weekStartsOn)\n        break\n      case 'End':\n        next = addDays(startOfWeek(focused, weekStartsOn), 6)\n        break\n      case 'PageUp':\n        next = addMonths(focused, -big)\n        break\n      case 'PageDown':\n        next = addMonths(focused, big)\n        break\n      case 'Escape':\n        // Inline, Esc drops a half-picked range. In the popover, the dialog closes instead.\n        if (picking && !fixedLayout) {\n          e.preventDefault()\n          e.stopPropagation()\n          reset()\n        }\n        return\n      default:\n        return\n    }\n    e.preventDefault()\n    next = clamp(next)\n    moveFocus.current = true\n    setFocused(next)\n    showMonth(next)\n    if (picking) setPreview(next)\n  }\n\n  const step = (n: number) => {\n    const v = addMonths(view, n)\n    setView(v)\n    const last = endOfMonth(addMonths(v, monthCount - 1))\n    if (before(focused, v) || before(last, focused)) setFocused(clamp(addMonths(focused, n)))\n  }\n  const canBack = !minDay || before(startOfMonth(minDay), view)\n  const canForward = !maxDay || before(addMonths(view, monthCount - 1), startOfMonth(maxDay))\n\n  const apply = () => {\n    if (!draft || !previous) return\n    if (value === undefined) setApplied(draft)\n    if (compare === undefined) setAppliedCompare(compareOn)\n    onChange?.(draft)\n    if (compareOn !== appliedCompare) onCompareChange?.(compareOn)\n    onApply?.({ range: draft, preset: preset ?? 'custom', compare: compareOn, previous })\n  }\n  const cancel = () => {\n    reset()\n    onCancel?.()\n  }\n\n  // Quick ranges: a radio group, one tab stop, arrows move and pick.\n  const shownPresets = presets\n  const presetRefs = useRef<(HTMLButtonElement | null)[]>([])\n  const picked = preset ? shownPresets.indexOf(preset) : -1\n  const activePreset = picked >= 0 ? picked : Math.max(0, shownPresets.findIndex(presetAllowed))\n  const onPresetKey = (e: ReactKeyboardEvent, i: number) => {\n    const dir = e.key === 'ArrowDown' || e.key === 'ArrowRight' ? 1 : e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 0\n    const n = shownPresets.length\n    let to = -1\n    if (dir) {\n      for (let k = 1; k <= n; k++) {\n        const j = (i + dir * k + n) % n\n        if (presetAllowed(shownPresets[j])) {\n          to = j\n          break\n        }\n      }\n    } else if (e.key === 'Home' || e.key === 'End') {\n      const order = e.key === 'Home' ? shownPresets.keys() : [...shownPresets.keys()].reverse()\n      for (const j of order)\n        if (presetAllowed(shownPresets[j])) {\n          to = j\n          break\n        }\n    }\n    if (to < 0) return\n    e.preventDefault()\n    presetRefs.current[to]?.focus()\n    choosePreset(shownPresets[to])\n  }\n\n  const ids = { status: useId(), grid: useId() }\n  const thisYear = today.getFullYear()\n\n  return (\n    <div\n      ref={root}\n      className={cn('w-full min-w-0 rounded-[calc(var(--radius)*2+2px)] bg-card text-card-foreground', TILE_SHADOW, className)}\n    >\n      <div className={cn('flex min-w-0', layout.side ? 'flex-row' : 'flex-col')}>\n        {shownPresets.length > 0 && (\n          <div\n            role=\"radiogroup\"\n            aria-label={L.presets}\n            className={cn(\n              'flex shrink-0',\n              layout.side\n                ? 'w-44 flex-col gap-0.5 border-r border-border p-2'\n                : 'flex-wrap gap-1.5 border-b border-border p-3 sm:px-4',\n            )}\n          >\n            {shownPresets.map((p, i) => {\n              const on = preset === p\n              const allowed = presetAllowed(p)\n              return (\n                <button\n                  key={p}\n                  ref={(el) => {\n                    presetRefs.current[i] = el\n                  }}\n                  type=\"button\"\n                  role=\"radio\"\n                  aria-checked={on}\n                  disabled={!allowed}\n                  tabIndex={i === activePreset ? 0 : -1}\n                  onClick={() => {\n                    choosePreset(p)\n                    // Custom by pointer: go straight to the days.\n                    if (p === 'custom') {\n                      grids.current?.querySelector<HTMLButtonElement>('[data-roving]')?.focus()\n                    }\n                  }}\n                  onKeyDown={(e) => onPresetKey(e, i)}\n                  className={cn(\n                    'relative flex cursor-pointer items-center gap-1.5 text-left text-[13.5px] font-medium transition-[background-color,color,scale] duration-150 ease-out-quint active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:transition-none motion-reduce:active:scale-100',\n                    FOCUS,\n                    layout.side ? 'min-h-10 justify-between rounded-[var(--radius)] px-3' : 'min-h-9 rounded-full px-3',\n                    on\n                      ? layout.side\n                        ? 'bg-muted text-foreground'\n                        : 'bg-foreground text-background'\n                      : cn('text-muted-foreground hover:bg-muted hover:text-foreground', !layout.side && 'shadow-[inset_0_0_0_1px_var(--border)]'),\n                  )}\n                >\n                  {L[p]}\n                  {on && <Check aria-hidden className={cn('shrink-0', layout.side ? 'size-4' : 'size-3.5')} strokeWidth={2.25} />}\n                </button>\n              )\n            })}\n          </div>\n        )}\n\n        <div\n          ref={grids}\n          className=\"min-w-0 flex-1 px-3 pt-3 pb-2 sm:px-4 sm:pt-4\"\n          onPointerLeave={() => setPreview(null)}\n        >\n          <div className={cn('grid gap-x-6 gap-y-4', monthCount === 2 ? 'grid-cols-2' : 'grid-cols-1')}>\n            {Array.from({ length: monthCount }, (_, i) => {\n              const month = addMonths(view, i)\n              return (\n                <MonthGrid\n                  key={keyOf(month)}\n                  month={month}\n                  idBase={`${ids.grid}-${i}`}\n                  title={fm.month.format(month)}\n                  prev={\n                    i === 0 ? (\n                      <NavButton label={L.previousMonth} disabled={!canBack} onClick={() => step(-1)}>\n                        <ChevronLeft aria-hidden className=\"size-4\" />\n                      </NavButton>\n                    ) : null\n                  }\n                  next={\n                    i === monthCount - 1 ? (\n                      <NavButton label={L.nextMonth} disabled={!canForward} onClick={() => step(1)}>\n                        <ChevronRight aria-hidden className=\"size-4\" />\n                      </NavButton>\n                    ) : null\n                  }\n                  weekStartsOn={weekStartsOn}\n                  fm={fm}\n                  today={today}\n                  focused={focused}\n                  shown={shown}\n                  previous={compareOn ? previous : null}\n                  isDisabled={isDisabled}\n                  labels={L}\n                  onPick={pick}\n                  onHover={(d) => picking && setPreview(d)}\n                  onKeyDown={onGridKey}\n                  onFocusDay={setFocused}\n                />\n              )\n            })}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"flex flex-wrap items-center gap-x-4 gap-y-3 border-t border-border px-3 py-3 sm:px-4\">\n        <div className=\"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1\">\n          <Switch label={L.compare} checked={compareOn} onChange={setCompareOn} className=\"[&_label]:text-[13.5px]\" />\n          {previous && (\n            <span\n              className={cn(\n                'inline-flex items-center gap-2 text-[13.5px] tabular-nums transition-colors duration-150 motion-reduce:transition-none',\n                compareOn ? 'text-foreground' : 'text-muted-foreground',\n              )}\n            >\n              <i aria-hidden className={cn('h-0.5 w-3 rounded-full bg-[var(--chart-2)]', !compareOn && 'opacity-0')} />\n              {rangeText(locale, previous, thisYear)}\n            </span>\n          )}\n        </div>\n        <div className=\"ml-auto flex items-center gap-2\">\n          <button\n            type=\"button\"\n            onClick={cancel}\n            className={cn(\n              'inline-flex min-h-10 cursor-pointer items-center rounded-full bg-muted px-4 text-[13.5px] font-medium transition-[background-color,scale] duration-150 ease-out-quint hover:bg-[color-mix(in_oklab,var(--foreground)_12%,var(--card))] active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100',\n              FOCUS,\n            )}\n          >\n            {L.cancel}\n          </button>\n          <button\n            type=\"button\"\n            onClick={apply}\n            disabled={!draft}\n            className={cn(\n              'inline-flex min-h-10 cursor-pointer items-center rounded-full bg-foreground px-5 text-[13.5px] font-medium text-background transition-[opacity,scale] duration-150 ease-out-quint hover:opacity-90 active:scale-[0.96] disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:transition-none motion-reduce:active:scale-100',\n              FOCUS,\n            )}\n          >\n            {L.apply}\n          </button>\n        </div>\n      </div>\n      <span id={ids.status} role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {status}\n      </span>\n    </div>\n  )\n}\n\nfunction NavButton({ label, disabled, onClick, children }: { label: string; disabled: boolean; onClick: () => void; children: ReactNode }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={label}\n      disabled={disabled}\n      onClick={onClick}\n      className={cn(\n        'grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-muted-foreground transition-[background-color,color,scale] duration-150 ease-out-quint hover:bg-muted hover:text-foreground active:scale-[0.94] disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none motion-reduce:active:scale-100',\n        FOCUS,\n      )}\n    >\n      {children}\n    </button>\n  )\n}\n\ntype Shown = DateRange & { preview: boolean; anchor: Date | null }\n\ntype MonthGridProps = {\n  month: Date\n  idBase: string\n  title: string\n  prev: ReactNode\n  next: ReactNode\n  weekStartsOn: number\n  fm: ReturnType<typeof makeFormats>\n  today: Date\n  focused: Date\n  shown: Shown | null\n  previous: DateRange | null\n  isDisabled: (d: Date) => boolean\n  labels: DateRangePickerLabels\n  onPick: (d: Date) => void\n  onHover: (d: Date) => void\n  onKeyDown: (e: ReactKeyboardEvent) => void\n  onFocusDay: (d: Date) => void\n}\n\nfunction MonthGrid({\n  month,\n  idBase,\n  title,\n  prev,\n  next,\n  weekStartsOn,\n  fm,\n  today,\n  focused,\n  shown,\n  previous,\n  isDisabled,\n  labels: L,\n  onPick,\n  onHover,\n  onKeyDown,\n  onFocusDay,\n}: MonthGridProps) {\n  const gridStart = startOfWeek(month, weekStartsOn)\n  const last = endOfMonth(month).getDate()\n  const titleId = `${idBase}-title`\n  const focusInMonth = focused.getMonth() === month.getMonth() && focused.getFullYear() === month.getFullYear()\n\n  return (\n    <div className=\"mx-auto w-full max-w-[280px] min-w-0\">\n      <div className=\"mb-1 flex h-10 items-center gap-1\">\n        {prev ?? <span className=\"size-10 shrink-0\" />}\n        <p id={titleId} className=\"flex-1 text-center text-[14px] font-medium tracking-[-0.01em]\">\n          {title}\n        </p>\n        {next ?? <span className=\"size-10 shrink-0\" />}\n      </div>\n      <table role=\"grid\" aria-labelledby={titleId} className=\"w-full table-fixed border-separate border-spacing-x-0 border-spacing-y-1\">\n        <thead>\n          <tr>\n            {Array.from({ length: 7 }, (_, c) => {\n              const d = addDays(gridStart, c)\n              return (\n                <th\n                  key={c}\n                  scope=\"col\"\n                  abbr={fm.weekdayLong.format(d)}\n                  className=\"h-7 p-0 text-[11.5px] font-medium text-muted-foreground\"\n                >\n                  {fm.weekdayShort.format(d)}\n                </th>\n              )\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {Array.from({ length: 6 }, (_, r) => (\n            <tr key={r}>\n              {Array.from({ length: 7 }, (_, c) => {\n                const d = addDays(gridStart, r * 7 + c)\n                // Days of other months stay empty, but keep their height, so the panel does not jump between months.\n                if (d.getMonth() !== month.getMonth()) return <td key={c} role=\"gridcell\" className=\"h-10 p-0\" />\n                const disabled = isDisabled(d)\n                const isToday = sameDay(d, today)\n                const lo = shown?.start\n                const hi = shown?.end\n                const isStart = sameDay(d, lo)\n                const isEnd = sameDay(d, hi)\n                const inside = !!lo && !!hi && before(lo, d) && before(d, hi)\n                const single = isStart && isEnd\n                const preview = !!shown?.preview\n                // While picking, the first day is filled and the day under the pointer or focus gets a ring.\n                const filled = preview ? sameDay(d, shown?.anchor) : isStart || isEnd\n                const ringEnd = preview && !filled && (isStart || isEnd)\n                const inCompare = !!previous && !before(d, previous.start) && !before(previous.end, d)\n                const rowStart = c === 0 || d.getDate() === 1\n                const rowEnd = c === 6 || d.getDate() === last\n                const roving = focusInMonth ? sameDay(d, focused) : false\n                // The band: full cell inside the range, half a cell out of each end, rounded where a row ends.\n                let band: CSSProperties | null = null\n                let bandRound = ''\n                if (!single && (inside || isStart || isEnd)) {\n                  if (inside) band = { left: 0, right: 0 }\n                  else if (isStart && !rowEnd) band = { left: '50%', right: 0 }\n                  else if (isEnd && !rowStart) band = { left: 0, right: '50%' }\n                  bandRound = cn(inside && rowStart && 'rounded-l-full', inside && rowEnd && 'rounded-r-full')\n                }\n                const state = [\n                  preview ? (filled ? L.rangeStart : null) : isStart ? L.rangeStart : null,\n                  isEnd && !preview ? L.rangeEnd : null,\n                  inside && !preview ? L.inRange : null,\n                  inCompare ? L.inComparison : null,\n                  isToday ? L.isToday : null,\n                  disabled ? L.unavailable : null,\n                ].filter(Boolean)\n                const selected = !preview && (isStart || isEnd || inside)\n                return (\n                  <td key={c} role=\"gridcell\" aria-selected={selected} className=\"relative p-0\">\n                    {band && <span aria-hidden className={cn('absolute inset-y-0', preview ? BAND_PREVIEW : BAND, bandRound)} style={band} />}\n                    <button\n                      type=\"button\"\n                      data-date={iso(d)}\n                      data-roving={roving || undefined}\n                      tabIndex={roving ? 0 : -1}\n                      aria-disabled={disabled || undefined}\n                      aria-current={isToday ? 'date' : undefined}\n                      aria-label={[fm.full.format(d), ...state].join(', ')}\n                      onClick={() => onPick(d)}\n                      onPointerEnter={() => onHover(d)}\n                      onFocus={() => !roving && onFocusDay(d)}\n                      onKeyDown={onKeyDown}\n                      className={cn(\n                        'relative mx-auto grid aspect-square w-full max-w-10 cursor-pointer place-items-center rounded-full text-[13.5px] tabular-nums',\n                        'transition-[background-color,color,box-shadow,scale] duration-150 ease-out-quint active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100',\n                        FOCUS,\n                        filled\n                          ? 'bg-primary font-medium text-primary-foreground'\n                          : disabled\n                            ? 'cursor-not-allowed text-muted-foreground/45 active:scale-100'\n                            : 'text-foreground hover:bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)]',\n                        ringEnd && 'shadow-[inset_0_0_0_1.5px_var(--primary)]',\n                      )}\n                    >\n                      {fm.day.format(d)}\n                      {isToday && (\n                        <i\n                          aria-hidden\n                          className={cn('absolute bottom-[5px] left-1/2 size-1 -translate-x-1/2 rounded-full', filled ? 'bg-primary-foreground' : 'bg-primary')}\n                        />\n                      )}\n                      {inCompare && !isToday && (\n                        <i aria-hidden className=\"absolute bottom-[5px] left-1/2 h-0.5 w-3 -translate-x-1/2 rounded-full bg-[var(--chart-2)]\" />\n                      )}\n                    </button>\n                  </td>\n                )\n              })}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  )\n}\n\n// ─── Picker: trigger + popover ─────────────────────────────────────────────────────────────────────────────────\n\nconst GAP = 8\nconst EDGE = 8\n\n/** A button that shows the chosen range and opens the date range panel in a popover anchored to it. */\nexport function DateRangePicker({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  align = 'start',\n  className,\n  panelClassName,\n  value,\n  defaultValue = null,\n  onChange,\n  onApply,\n  onCancel,\n  compare,\n  defaultCompare = false,\n  onCompareChange,\n  ...panel\n}: DateRangePickerProps) {\n  const L = { ...LABELS, ...panel.labels }\n  const locale = panel.locale ?? 'en'\n  const [mountedAt] = useState(() => new Date())\n  const today = dayOf(panel.today ?? mountedAt)\n  const reduced = useReducedMotion()\n\n  const [applied, setApplied] = useControllable<DateRange | null>(value, defaultValue)\n  const [appliedCompare, setAppliedCompare] = useControllable<boolean>(compare, defaultCompare)\n  const [open, setOwnOpen] = useControllable<boolean>(openProp, defaultOpen)\n  const setOpen = (next: boolean) => {\n    setOwnOpen(next)\n    onOpenChange?.(next)\n  }\n\n  const trigger = useRef<HTMLButtonElement>(null)\n  const dialog = useRef<HTMLDialogElement>(null)\n  const [instant, setInstant] = useState(false)\n  const [vw, setVw] = useState(() => (typeof window === 'undefined' ? 1024 : document.documentElement.clientWidth))\n  const room = vw - 2 * EDGE\n  const layout = layoutFor(room)\n  const width = layout.side ? SIDE_MIN + 4 : layout.months === 2 ? Math.min(room, 640) : Math.min(room, 360)\n\n  const place = useCallback(() => {\n    const t = trigger.current\n    const d = dialog.current\n    if (!t || !d || !d.open) return\n    const r = t.getBoundingClientRect()\n    const w = d.offsetWidth\n    const h = d.offsetHeight\n    const W = document.documentElement.clientWidth\n    const H = innerHeight\n    const left = Math.min(Math.max(align === 'end' ? r.right - w : r.left, EDGE), Math.max(EDGE, W - w - EDGE))\n    const below = H - r.bottom - GAP - EDGE\n    const above = r.top - GAP - EDGE\n    const up = h > below && above > below\n    const top = Math.min(Math.max(up ? r.top - GAP - h : r.bottom + GAP, EDGE), Math.max(EDGE, H - h - EDGE))\n    d.style.left = `${left}px`\n    d.style.top = `${top}px`\n    // The popover grows from the corner nearest its button.\n    const x = Math.min(Math.max((align === 'end' ? r.right : r.left) - left, 0), w)\n    d.style.transformOrigin = `${x}px ${up ? '100%' : '0'}`\n  }, [align])\n\n  useLayoutEffect(() => {\n    const d = dialog.current\n    if (!d) return\n    if (open && !d.open) {\n      d.showModal()\n      place()\n      d.querySelector<HTMLButtonElement>('[data-roving]')?.focus()\n    } else if (!open && d.open) {\n      d.close()\n    }\n  }, [open, place])\n\n  // Track the width even while closed, so the popover opens in the layout that fits now.\n  useEffect(() => {\n    const onResize = () => setVw(document.documentElement.clientWidth)\n    addEventListener('resize', onResize)\n    return () => removeEventListener('resize', onResize)\n  }, [])\n  useEffect(() => {\n    if (!open) return\n    addEventListener('resize', place)\n    addEventListener('scroll', place, true)\n    return () => {\n      removeEventListener('resize', place)\n      removeEventListener('scroll', place, true)\n    }\n  }, [open, place])\n  useLayoutEffect(() => place(), [width, place])\n\n  const close = () => {\n    setOpen(false)\n    // Back to the button that opened it, after the dialog has left the top layer.\n    requestAnimationFrame(() => trigger.current?.focus())\n  }\n\n  // Tab and Shift+Tab stay inside the popover.\n  const onDialogKey = (e: ReactKeyboardEvent<HTMLDialogElement>) => {\n    if (e.key !== 'Tab') return\n    const items = [...e.currentTarget.querySelectorAll<HTMLElement>('button:not([disabled]):not([tabindex=\"-1\"]), [tabindex=\"0\"]')]\n    if (!items.length) return\n    const firstItem = items[0]\n    const lastItem = items[items.length - 1]\n    if (e.shiftKey && document.activeElement === firstItem) {\n      e.preventDefault()\n      lastItem.focus()\n    } else if (!e.shiftKey && document.activeElement === lastItem) {\n      e.preventDefault()\n      firstItem.focus()\n    }\n  }\n\n  const presets = panel.presets ?? ALL_PRESETS\n  const appliedPreset = matchPreset(applied, today, presets)\n  const text = applied ? rangeText(locale, applied) : L.placeholder\n  const compared = applied && appliedCompare ? rangeText(locale, previousPeriod(applied), today.getFullYear()) : null\n\n  return (\n    <>\n      <button\n        ref={trigger}\n        type=\"button\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        onClick={(e) => {\n          // A click from Enter or Space has no pointer position; the popover then opens without motion.\n          setInstant(e.detail === 0)\n          setOpen(!open)\n        }}\n        className={cn(\n          'inline-flex min-h-11 max-w-full cursor-pointer items-center gap-2.5 rounded-full bg-card px-4 text-[13.5px] font-medium text-card-foreground transition-[background-color,scale] duration-150 ease-out-quint hover:bg-muted active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100',\n          TILE_SHADOW,\n          FOCUS,\n          className,\n        )}\n      >\n        <CalendarDays aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n        <span className=\"sr-only\">{L.trigger}: </span>\n        {appliedPreset && appliedPreset !== 'custom' && (\n          <>\n            <span className=\"hidden whitespace-nowrap text-muted-foreground sm:inline\">{L[appliedPreset]}</span>\n            <span aria-hidden className=\"hidden h-3.5 w-px bg-border sm:inline-block\" />\n          </>\n        )}\n        <span className={cn('min-w-0 truncate tabular-nums', !applied && 'text-muted-foreground')}>{text}</span>\n        {compared && (\n          <span className=\"hidden whitespace-nowrap text-muted-foreground tabular-nums md:inline\">\n            {L.versus} {compared}\n          </span>\n        )}\n        <ChevronDown\n          aria-hidden\n          className={cn('size-4 shrink-0 text-muted-foreground transition-transform duration-200 ease-out-quint motion-reduce:transition-none', open && 'rotate-180')}\n        />\n      </button>\n      <dialog\n        ref={dialog}\n        aria-label={L.dialog}\n        onKeyDown={onDialogKey}\n        onCancel={(e) => {\n          e.preventDefault()\n          onCancel?.()\n          close()\n        }}\n        onClose={() => open && setOpen(false)}\n        onClick={(e) => {\n          if (e.target === dialog.current) {\n            onCancel?.()\n            close()\n          }\n        }}\n        style={{ width }}\n        className={cn(\n          'fixed inset-auto m-0 max-h-[calc(100dvh-16px)] max-w-none overflow-auto bg-transparent p-0 text-card-foreground backdrop:bg-transparent',\n          'rounded-[calc(var(--radius)*2+2px)] shadow-[0_16px_48px_-12px_rgba(0,0,0,0.28),0_4px_12px_-4px_rgba(0,0,0,0.12)]',\n          !(instant || reduced) && 'scale-100 opacity-100 transition-[opacity,scale] duration-[160ms] ease-out-quint starting:scale-[0.97] starting:opacity-0',\n        )}\n      >\n        {open && (\n          <Panel\n            {...panel}\n            today={today}\n            layout={layout}\n            value={applied}\n            compare={appliedCompare}\n            className={panelClassName}\n            onApply={(detail) => {\n              if (value === undefined) setApplied(detail.range)\n              if (compare === undefined) setAppliedCompare(detail.compare)\n              onChange?.(detail.range)\n              if (detail.compare !== appliedCompare) onCompareChange?.(detail.compare)\n              onApply?.(detail)\n              close()\n            }}\n            onCancel={() => {\n              onCancel?.()\n              close()\n            }}\n          />\n        )}\n      </dialog>\n    </>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/manniche/date-range-picker/dates.ts",
      "content": "// Small helpers for local calendar days, used by DateRangePicker. Every helper works on year, month and day\n// (new Date(y, m, d)), never on milliseconds, so a day never shifts across time zones or daylight saving.\n\n/** A range of whole days, both ends included, as Dates at local midnight. */\nexport type DateRange = { start: Date; end: Date }\n\n/** The quick ranges. \"custom\" is the one that is active after picking days by hand. */\nexport type DateRangePreset = 'today' | 'yesterday' | 'last7' | 'last30' | 'thisMonth' | 'lastMonth' | 'custom'\n\nexport const ymd = (y: number, m: number, d: number) => new Date(y, m, d)\nexport const dayOf = (d: Date) => ymd(d.getFullYear(), d.getMonth(), d.getDate())\nexport const addDays = (d: Date, n: number) => ymd(d.getFullYear(), d.getMonth(), d.getDate() + n)\nexport const startOfMonth = (d: Date) => ymd(d.getFullYear(), d.getMonth(), 1)\nexport const endOfMonth = (d: Date) => ymd(d.getFullYear(), d.getMonth() + 1, 0)\n/** Moves by whole months and keeps the day, cut to the month's length (31 Jan + 1 month = 28 or 29 Feb). */\nexport function addMonths(d: Date, n: number) {\n  const first = ymd(d.getFullYear(), d.getMonth() + n, 1)\n  return ymd(first.getFullYear(), first.getMonth(), Math.min(d.getDate(), endOfMonth(first).getDate()))\n}\n/** A number that sorts like the day: 20261014. */\nexport const keyOf = (d: Date) => d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate()\nexport const sameDay = (a: Date | null | undefined, b: Date | null | undefined) => !!a && !!b && keyOf(a) === keyOf(b)\nexport const before = (a: Date, b: Date) => keyOf(a) < keyOf(b)\n/** Whole days from a to b, counted on the calendar, so daylight saving never adds or loses one. */\nexport const daysBetween = (a: Date, b: Date) =>\n  Math.round((Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()) - Date.UTC(a.getFullYear(), a.getMonth(), a.getDate())) / 864e5)\nexport const startOfWeek = (d: Date, ws: number) => addDays(d, -((d.getDay() - ws + 7) % 7))\nexport const monthsBetween = (a: Date, b: Date) => (b.getFullYear() - a.getFullYear()) * 12 + b.getMonth() - a.getMonth()\nexport const iso = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`\nexport const ordered = (a: Date, b: Date): DateRange => (before(b, a) ? { start: b, end: a } : { start: a, end: b })\n\n/** The period of equal length that ends the day before the range starts. */\nexport function previousPeriod(range: DateRange): DateRange {\n  const length = daysBetween(range.start, range.end) + 1\n  return { start: addDays(range.start, -length), end: addDays(range.start, -1) }\n}\n\n/** The range a quick range stands for, counted from `today`. \"This month\" runs to today; \"custom\" has none. */\nexport function presetRange(preset: DateRangePreset, today: Date = new Date()): DateRange | null {\n  const t = dayOf(today)\n  switch (preset) {\n    case 'today':\n      return { start: t, end: t }\n    case 'yesterday': {\n      const y = addDays(t, -1)\n      return { start: y, end: y }\n    }\n    case 'last7':\n      return { start: addDays(t, -6), end: t }\n    case 'last30':\n      return { start: addDays(t, -29), end: t }\n    case 'thisMonth':\n      return { start: startOfMonth(t), end: t }\n    case 'lastMonth': {\n      const s = addMonths(startOfMonth(t), -1)\n      return { start: s, end: endOfMonth(s) }\n    }\n    default:\n      return null\n  }\n}\n\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/manniche/hooks/use-reduced-motion.ts",
      "content": "import { useSyncExternalStore } from 'react'\n\nconst QUERY = '(prefers-reduced-motion: reduce)'\n\nfunction subscribe(onChange: () => void) {\n  const mq = window.matchMedia(QUERY)\n  mq.addEventListener('change', onChange)\n  return () => mq.removeEventListener('change', onChange)\n}\n\n/** True when the visitor has asked the system for less motion. */\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(QUERY).matches,\n    () => false,\n  )\n}\n",
      "type": "registry:hook"
    }
  ],
  "meta": {
    "tier": "free",
    "added": "2026-10-14",
    "usedIn": [],
    "props": [
      {
        "name": "value",
        "type": "DateRange | null",
        "default": null,
        "required": false,
        "description": "The applied range (controlled). `null` for none. Leave it out to let the panel keep track."
      },
      {
        "name": "defaultValue",
        "type": "DateRange | null",
        "default": "null",
        "required": false,
        "description": "The range applied at the start when not controlled."
      },
      {
        "name": "onChange",
        "type": "(range: DateRange) => void",
        "default": null,
        "required": false,
        "description": "Called with the range when Apply is pressed."
      },
      {
        "name": "onApply",
        "type": "(detail: DateRangeApplyDetail) => void",
        "default": null,
        "required": false,
        "description": "Called on Apply with the range, the active quick range, the compare state and the previous period."
      },
      {
        "name": "onCancel",
        "type": "() => void",
        "default": null,
        "required": false,
        "description": "Called on Cancel. The panel goes back to the applied range."
      },
      {
        "name": "compare",
        "type": "boolean",
        "default": null,
        "required": false,
        "description": "Whether comparing to the previous period is on (controlled). Changes are handed back on Apply."
      },
      {
        "name": "defaultCompare",
        "type": "boolean",
        "default": "false",
        "required": false,
        "description": "Whether compare starts on when not controlled. Default false."
      },
      {
        "name": "onCompareChange",
        "type": "(compare: boolean) => void",
        "default": null,
        "required": false,
        "description": "Called with the new compare state on Apply, when it changed."
      },
      {
        "name": "presets",
        "type": "DateRangePreset[]",
        "default": null,
        "required": false,
        "description": "Which quick ranges to show, in order. Default all seven. Leave \"custom\" out to hide it."
      },
      {
        "name": "weekStartsOn",
        "type": "Weekday",
        "default": null,
        "required": false,
        "description": "The first day of the week, 0 for Sunday to 6 for Saturday. Default 1 (Monday)."
      },
      {
        "name": "locale",
        "type": "string",
        "default": null,
        "required": false,
        "description": "BCP 47 locale for month and weekday names and the range text, such as \"de\" or \"en-GB\". Default \"en\"."
      },
      {
        "name": "min",
        "type": "Date",
        "default": null,
        "required": false,
        "description": "The earliest day that can be picked."
      },
      {
        "name": "max",
        "type": "Date",
        "default": null,
        "required": false,
        "description": "The last day that can be picked, such as today for data that only goes back."
      },
      {
        "name": "isDateDisabled",
        "type": "(date: Date) => boolean",
        "default": null,
        "required": false,
        "description": "Return true for days that cannot be picked. A range may still run across them."
      },
      {
        "name": "today",
        "type": "Date",
        "default": null,
        "required": false,
        "description": "The day the quick ranges count from. Default the real today; pass one to pin it in tests and demos."
      },
      {
        "name": "months",
        "type": "1 | 2",
        "default": null,
        "required": false,
        "description": "Show one or two months. Default: two, or one when the panel is narrower than about 640 px."
      },
      {
        "name": "labels",
        "type": "Partial<DateRangePickerLabels>",
        "default": null,
        "required": false,
        "description": "Text for other languages. Missing keys fall back to English."
      },
      {
        "name": "className",
        "type": "string",
        "default": null,
        "required": false,
        "description": "Classes for the outer panel."
      },
      {
        "name": "open",
        "type": "boolean",
        "default": null,
        "required": false,
        "description": "Whether the popover is open (controlled)."
      },
      {
        "name": "defaultOpen",
        "type": "boolean",
        "default": "false",
        "required": false,
        "description": "Whether the popover starts open when not controlled."
      },
      {
        "name": "onOpenChange",
        "type": "(open: boolean) => void",
        "default": null,
        "required": false,
        "description": "Called with the new open state."
      },
      {
        "name": "align",
        "type": "'start' | 'end'",
        "default": "'start'",
        "required": false,
        "description": "Which edge of the button the popover lines up with. Default \"start\"."
      },
      {
        "name": "panelClassName",
        "type": "string",
        "default": null,
        "required": false,
        "description": "Classes for the popover panel."
      }
    ],
    "a11y": [
      {
        "topic": "keyboard",
        "text": "The days form a grid with one tab stop. The arrows move by day and week, Home and End go to the start and end of the week, and PageUp and PageDown move a month (with Shift, a year). Enter or Space picks the start and then the end. The quick ranges are a radio group: the arrows move and pick. In the popover, Tab stays inside, and Escape closes it and puts focus back on the button. Inline, Escape drops a half-picked range.",
        "keys": [
          "Arrow keys",
          "Home",
          "End",
          "PageUp",
          "PageDown",
          "Shift+PageUp",
          "Shift+PageDown",
          "Enter",
          "Space",
          "Tab",
          "Escape"
        ]
      },
      {
        "topic": "screen-reader",
        "text": "Each month is a grid labelled by its name, with full weekday names on the column headers. Each day reads its full date, then start or end of range, in range, in the comparison period, today or unavailable. Picked days are aria-selected, and today has aria-current=\"date\". A polite status says what was picked: \"Start October 5. Now pick the end date.\" and then \"Oct 5 – 9, 2026, 5 days.\" The button reads \"Date range:\" and the range, and has aria-haspopup=\"dialog\" and aria-expanded. The popover is a modal dialog, so the page behind it is inert."
      },
      {
        "topic": "motion",
        "text": "The popover fades and scales in from 97% over 160 ms. It does not animate when opened with the keyboard, or when the system asks for less motion. Days change colour over 150 ms, and nothing else moves. Under reduced motion every transition is off, and the panel works the same."
      },
      {
        "topic": "other",
        "text": "Dates are local calendar days (year, month, day) with no library and no milliseconds, so nothing shifts across time zones or daylight saving. Month and weekday names come from Intl for the given locale. Days outside min and max, and days rejected by isDateDisabled, cannot be picked, and quick ranges that would land on them are disabled. All text can be replaced through labels."
      }
    ]
  },
  "categories": [
    "controls"
  ],
  "type": "registry:component"
}