{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-calendar",
  "title": "Date range calendar",
  "description": "A Monday-first range calendar that previews the span under the cursor, greys out days outside its bounds and jumps by month or typed year; plus a period dropdown whose \"Custom range\" step widens the same popover into the calendar.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/date-range-calendar/date-range-calendar.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Check, ChevronDown, ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotionConfig } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A range calendar drawn in the page's own visual language rather than the\n * operating system's: one control for one intention (\"these dates\"), picked in\n * two clicks on one surface with the span previewed under the cursor.\n *\n * - It cannot dismiss its own parent. A native date picker is browser chrome,\n *   so focus leaving into it reads as focus leaving a popover that closes on\n *   blur. Every element here is a real DOM node inside that subtree.\n * - Days outside `min`/`max` are visibly unavailable, not silently refused.\n * - en-GB, Monday-first, always, whatever the machine's locale says.\n *\n * Everything is a UTC ISO day (`YYYY-MM-DD`): ISO days sort and compare\n * correctly as plain strings, and a local-midnight Date would put a London\n * user's \"1 September\" an hour before the UTC one.\n *\n * Keyboard: arrows move by a day, PageUp/PageDown by a month, Home/End to the\n * ends of the week, Enter or Space picks. One tab stop with a roving tabIndex,\n * so a keyboard user does not tab through 42 cells.\n */\n\n/* ─── Date rules (pure) ──────────────────────────────────────────────────── */\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** The house curve: front-loaded, so a press is answered at once. */\nconst EASE = [0.2, 0.7, 0.2, 1] as const;\n/** EASE mirrored: gentle out of rest, accelerating away. For closing. */\nconst COLLAPSE_EASE = [0.8, 0, 0.3, 0.8] as const;\n\n/** Monday-first. */\nconst WEEKDAY_LABELS = [\"M\", \"T\", \"W\", \"T\", \"F\", \"S\", \"S\"] as const;\n\n/**\n * Always six rows. A month needing five would otherwise make the calendar\n * shorter, and a popover that animates its height would resize every time you\n * paged between months.\n */\nconst GRID_CELLS = 6 * 7;\n\nconst MONTH_NAMES = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n] as const;\n\n/** `YYYY-MM`, the calendar's paging unit. */\ntype MonthKey = string;\n\ntype DayCell = { iso: string; dayOfMonth: number; inMonth: boolean };\n\n/** A range in progress or complete. Both ends are UTC ISO days, or null. */\nexport type RangeSelection = { from: string | null; to: string | null };\n\n/** A one-click shortcut shown under the grid. */\nexport type CalendarPreset = { label: string; from: string; to: string };\n\nfunction toIsoDay(ms: number): string {\n  return new Date(ms).toISOString().slice(0, 10);\n}\n\nfunction todayIso(now: Date = new Date()): string {\n  return toIsoDay(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));\n}\n\nfunction monthKeyOf(iso: string): MonthKey {\n  return iso.slice(0, 7);\n}\n\nfunction addMonths(month: MonthKey, delta: number): MonthKey {\n  const year = Number(month.slice(0, 4));\n  const index = Number(month.slice(5, 7)) - 1;\n  return toIsoDay(Date.UTC(year, index + delta, 1)).slice(0, 7);\n}\n\n/** \"2026-03-31\" into February becomes \"2026-02-28\". */\nfunction clampDayToMonth(dayIso: string, month: MonthKey): string {\n  const year = Number(month.slice(0, 4));\n  const index = Number(month.slice(5, 7));\n  const day = Number(dayIso.slice(8, 10));\n  const lastDay = new Date(Date.UTC(year, index, 0)).getUTCDate();\n  return `${month}-${String(Math.min(day, lastDay)).padStart(2, \"0\")}`;\n}\n\nfunction monthLabel(month: MonthKey): string {\n  return `${MONTH_NAMES[Number(month.slice(5, 7)) - 1]} ${month.slice(0, 4)}`;\n}\n\nfunction formatDayLong(iso: string): string {\n  return new Date(`${iso}T00:00:00Z`).toLocaleDateString(\"en-GB\", {\n    weekday: \"long\",\n    day: \"numeric\",\n    month: \"long\",\n    year: \"numeric\",\n    timeZone: \"UTC\",\n  });\n}\n\n/** The 42 cells of a month, Monday-first, borrowing days either side. */\nfunction buildMonthGrid(month: MonthKey): DayCell[] {\n  const year = Number(month.slice(0, 4));\n  const index = Number(month.slice(5, 7)) - 1;\n  const first = Date.UTC(year, index, 1);\n  const leading = (new Date(first).getUTCDay() + 6) % 7;\n  const start = first - leading * DAY_MS;\n  return Array.from({ length: GRID_CELLS }, (_, i) => {\n    const ms = start + i * DAY_MS;\n    const date = new Date(ms);\n    return {\n      iso: toIsoDay(ms),\n      dayOfMonth: date.getUTCDate(),\n      inMonth: date.getUTCMonth() === index && date.getUTCFullYear() === year,\n    };\n  });\n}\n\ntype DayFlags = {\n  isStart: boolean;\n  isEnd: boolean;\n  isInside: boolean;\n  isPreview: boolean;\n  isToday: boolean;\n  isDisabled: boolean;\n};\n\n/**\n * What one day looks like now. With a start picked and no end, the hovered day\n * stands in for the end, so the user sees the span before committing to it.\n */\nfunction dayFlags(\n  iso: string,\n  ctx: {\n    selection: RangeSelection;\n    hover: string | null;\n    min?: string | null;\n    max?: string | null;\n    today: string;\n  },\n): DayFlags {\n  const { selection, hover, min, max, today } = ctx;\n  const isDisabled = Boolean((min && iso < min) || (max && iso > max));\n  const previewEnd =\n    selection.from && !selection.to && hover && !(min && hover < min) && !(max && hover > max)\n      ? hover\n      : null;\n  const end = selection.to ?? previewEnd;\n\n  if (!selection.from || !end) {\n    return {\n      isStart: selection.from === iso,\n      isEnd: false,\n      isInside: false,\n      isPreview: false,\n      isToday: today === iso,\n      isDisabled,\n    };\n  }\n\n  const lo = selection.from <= end ? selection.from : end;\n  const hi = selection.from <= end ? end : selection.from;\n  const inRange = iso >= lo && iso <= hi;\n  return {\n    isStart: iso === lo,\n    isEnd: iso === hi,\n    isInside: inRange && iso !== lo && iso !== hi,\n    isPreview: previewEnd !== null,\n    isToday: today === iso,\n    isDisabled,\n  };\n}\n\n/**\n * The click rule. Nothing picked, or a full range picked: start over here. A\n * start picked and this day on or after it: close the range. A day BEFORE the\n * start begins a new range rather than silently swapping the ends, because the\n * user clicked meaning \"the range begins here\".\n */\nfunction nextSelection(current: RangeSelection, clicked: string): RangeSelection {\n  if (!current.from || current.to) return { from: clicked, to: null };\n  if (clicked < current.from) return { from: clicked, to: null };\n  return { from: current.from, to: clicked };\n}\n\n/** Usable once both ends exist and run the right way round. */\nexport function isCompleteRange(\n  selection: RangeSelection,\n): selection is { from: string; to: string } {\n  return Boolean(selection.from && selection.to && selection.from <= selection.to);\n}\n\nfunction rangeLengthDays(from: string, to: string): number {\n  return Math.floor((Date.parse(`${to}T00:00:00Z`) - Date.parse(`${from}T00:00:00Z`)) / DAY_MS) + 1;\n}\n\n/**\n * Calendar periods, as opposed to a rolling \"past 30 days\": \"this month\" is\n * what a report is written against.\n */\nexport function calendarPresets(now: Date = new Date()): CalendarPreset[] {\n  const year = now.getUTCFullYear();\n  const month = now.getUTCMonth();\n  const today = todayIso(now);\n  const firstOf = (y: number, m: number) => toIsoDay(Date.UTC(y, m, 1));\n  const lastOf = (y: number, m: number) => toIsoDay(Date.UTC(y, m + 1, 1) - DAY_MS);\n  return [\n    { label: \"This month\", from: firstOf(year, month), to: today },\n    { label: \"Last month\", from: firstOf(year, month - 1), to: lastOf(year, month - 1) },\n    { label: \"This quarter\", from: firstOf(year, Math.floor(month / 3) * 3), to: today },\n  ];\n}\n\n/**\n * Clamps a preset to the bounds, or drops it when it falls wholly outside: a\n * shortcut that produces an empty chart reads as broken data.\n */\nfunction clampPreset(\n  preset: CalendarPreset,\n  min?: string | null,\n  max?: string | null,\n): CalendarPreset | null {\n  const from = min && preset.from < min ? min : preset.from;\n  const to = max && preset.to > max ? max : preset.to;\n  if (from > to) return null;\n  return { ...preset, from, to };\n}\n\n/**\n * Keeps an absolutely positioned panel on screen. Use as the panel's ref; it\n * nudges the panel back with margins (not transforms, which Tailwind and Motion\n * both own) when an edge comes within 12px of the viewport.\n */\nexport function keepInViewport(el: HTMLElement | null): (() => void) | undefined {\n  if (!el) return;\n  const EDGE = 12;\n  const fit = () => {\n    el.style.marginLeft = \"\";\n    el.style.marginRight = \"\";\n    const { left, right } = el.getBoundingClientRect();\n    const width = document.documentElement.clientWidth;\n    let shift = 0;\n    if (right > width - EDGE) shift = width - EDGE - right;\n    if (left + shift < EDGE) shift = EDGE - left;\n    if (shift === 0) return;\n    el.style.marginLeft = `${shift}px`;\n    el.style.marginRight = `${-shift}px`;\n  };\n  fit();\n  const observer = new ResizeObserver(fit);\n  observer.observe(el);\n  window.addEventListener(\"resize\", fit);\n  return () => {\n    observer.disconnect();\n    window.removeEventListener(\"resize\", fit);\n  };\n}\n\n/* ─── The calendar ───────────────────────────────────────────────────────── */\n\n/**\n * Cells fill their grid column rather than carrying a fixed width, so the range\n * track joins into one continuous bar instead of a row of separate blocks.\n */\nconst CELL = \"h-[30px] w-full\";\n\nconst NAV_BUTTON =\n  \"grid h-7 w-7 shrink-0 place-items-center rounded-lg text-dim transition-colors hover:bg-paper hover:text-ink disabled:pointer-events-none disabled:opacity-25 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand\";\n\nconst PANEL =\n  \"absolute left-1/2 top-9 z-30 w-[270px] max-w-[calc(100vw-2rem)] -translate-x-1/2 rounded-xl border border-rule bg-popover p-2.5 shadow-xl backdrop-blur-md\";\n\nexport type DateRangeCalendarProps = {\n  /** Committed selection; drives what is drawn, and where the calendar opens. */\n  value: RangeSelection;\n  onChange: (selection: RangeSelection) => void;\n  /**\n   * `\"range\"` (default) picks a span in two clicks; `\"single\"` picks one day,\n   * reported as `{ from: day, to: day }`.\n   */\n  mode?: \"range\" | \"single\";\n  /** Inclusive bounds (ISO days). Days outside are shown but not selectable. */\n  min?: string | null;\n  max?: string | null;\n  /** Fixes \"today\"; defaults to the real clock. */\n  now?: Date;\n  /** Shows the shortcut row under the grid. Forced off in single mode. */\n  showPresets?: boolean;\n  /** Shortcuts to offer. Defaults to This month / Last month / This quarter. */\n  presetOptions?: CalendarPreset[];\n  className?: string;\n};\n\nexport function DateRangeCalendar({\n  value,\n  onChange,\n  mode = \"range\",\n  min,\n  max,\n  now,\n  showPresets = true,\n  presetOptions,\n  className,\n}: DateRangeCalendarProps) {\n  const reduceMotion = useReducedMotionConfig();\n  const today = useMemo(() => todayIso(now ?? new Date()), [now]);\n\n  // Open on the start's month, else the newest month the bounds allow, else\n  // today's, so a bounded calendar never opens on a page of greyed-out days.\n  const [month, setMonth] = useState<MonthKey>(() =>\n    monthKeyOf(value.from ?? (max && max < today ? max : today)),\n  );\n  const [hover, setHover] = useState<string | null>(null);\n  const [focusedDay, setFocusedDay] = useState<string>(() => value.from ?? today);\n  const gridRef = useRef<HTMLDivElement>(null);\n\n  const [activeDropdown, setActiveDropdown] = useState<\"month\" | \"year\" | null>(null);\n  const [yearSearch, setYearSearch] = useState(\"\");\n  const containerRef = useRef<HTMLDivElement>(null);\n  const yearInputRef = useRef<HTMLInputElement>(null);\n  const activeYearRef = useRef<HTMLButtonElement>(null);\n\n  const currentYear = Number(month.slice(0, 4));\n  const currentMonthIndex = Number(month.slice(5, 7)) - 1;\n  const currentMonthName = MONTH_NAMES[currentMonthIndex] ?? \"Month\";\n\n  const minYear = min ? Number(min.slice(0, 4)) : 1940;\n  const maxYear = max ? Number(max.slice(0, 4)) : new Date().getUTCFullYear() + 5;\n\n  const allYears = useMemo(() => {\n    const years: number[] = [];\n    for (let y = maxYear; y >= minYear; y -= 1) years.push(y);\n    return years;\n  }, [minYear, maxYear]);\n\n  const filteredYears = useMemo(() => {\n    const query = yearSearch.trim();\n    return query ? allYears.filter((y) => String(y).includes(query)) : allYears;\n  }, [allYears, yearSearch]);\n\n  useEffect(() => {\n    if (activeDropdown !== \"year\") return;\n    requestAnimationFrame(() => {\n      yearInputRef.current?.focus();\n      yearInputRef.current?.select();\n      activeYearRef.current?.scrollIntoView({ block: \"center\", behavior: \"auto\" });\n    });\n  }, [activeDropdown]);\n\n  useEffect(() => {\n    if (!activeDropdown) return;\n    const onPointerDown = (event: MouseEvent) => {\n      if (containerRef.current && !containerRef.current.contains(event.target as Node)) {\n        setActiveDropdown(null);\n      }\n    };\n    // Escape closes the month/year panel only, not a popover around the calendar.\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        event.stopPropagation();\n        event.preventDefault();\n        setActiveDropdown(null);\n      }\n    };\n    document.addEventListener(\"mousedown\", onPointerDown);\n    document.addEventListener(\"keydown\", onKeyDown, true);\n    return () => {\n      document.removeEventListener(\"mousedown\", onPointerDown);\n      document.removeEventListener(\"keydown\", onKeyDown, true);\n    };\n  }, [activeDropdown]);\n\n  const jumpTo = (next: MonthKey) => {\n    setMonth(next);\n    setFocusedDay((prev) => clampDayToMonth(prev, next));\n    setActiveDropdown(null);\n  };\n\n  const handleYearSearchSubmit = (event?: React.FormEvent) => {\n    event?.preventDefault();\n    const year = Number(yearSearch.trim());\n    const pick =\n      !Number.isNaN(year) && year >= minYear && year <= maxYear ? year : filteredYears[0];\n    if (pick !== undefined) {\n      jumpTo(`${pick}-${String(currentMonthIndex + 1).padStart(2, \"0\")}`);\n    }\n  };\n\n  /** Split into weeks so the grid carries real role=\"row\" elements. */\n  const weeks = useMemo(() => {\n    const cells = buildMonthGrid(month);\n    const rows: DayCell[][] = [];\n    for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7));\n    return rows;\n  }, [month]);\n\n  const presets = useMemo(() => {\n    if (!showPresets || mode === \"single\") return [];\n    return (presetOptions ?? calendarPresets(now ?? new Date()))\n      .map((preset) => clampPreset(preset, min, max))\n      .filter((preset): preset is CalendarPreset => preset !== null);\n  }, [showPresets, mode, now, presetOptions, min, max]);\n\n  // Paging is disabled at the edges rather than allowed and then empty.\n  const previousMonth = addMonths(month, -1);\n  const nextMonth = addMonths(month, 1);\n  const canGoBack = !min || `${previousMonth}-31` >= min;\n  const canGoForward = !max || `${nextMonth}-01` <= max;\n\n  const selectDay = (iso: string) => {\n    onChange(mode === \"single\" ? { from: iso, to: iso } : nextSelection(value, iso));\n    setFocusedDay(iso);\n  };\n\n  /** Moves focus by whole days, paging the visible month when it crosses out. */\n  const moveFocus = (deltaDays: number, deltaMonths = 0) => {\n    let target = focusedDay;\n    if (deltaMonths !== 0) {\n      target = clampDayToMonth(focusedDay, addMonths(monthKeyOf(focusedDay), deltaMonths));\n    }\n    if (deltaDays !== 0) {\n      target = toIsoDay(Date.parse(`${target}T00:00:00Z`) + deltaDays * DAY_MS);\n    }\n    if (min && target < min) target = min;\n    if (max && target > max) target = max;\n\n    setFocusedDay(target);\n    if (monthKeyOf(target) !== month) setMonth(monthKeyOf(target));\n    requestAnimationFrame(() => {\n      gridRef.current?.querySelector<HTMLButtonElement>(`[data-day=\"${target}\"]`)?.focus();\n    });\n  };\n\n  const onGridKeyDown = (event: React.KeyboardEvent) => {\n    const weekday = (new Date(`${focusedDay}T00:00:00Z`).getUTCDay() + 6) % 7;\n    const keyed: Record<string, () => void> = {\n      ArrowLeft: () => moveFocus(-1),\n      ArrowRight: () => moveFocus(1),\n      ArrowUp: () => moveFocus(-7),\n      ArrowDown: () => moveFocus(7),\n      PageUp: () => moveFocus(0, -1),\n      PageDown: () => moveFocus(0, 1),\n      Home: () => moveFocus(-weekday),\n      End: () => moveFocus(6 - weekday),\n    };\n    const handler = keyed[event.key];\n    if (handler) {\n      event.preventDefault();\n      // Navigation keys stop here; Escape is left to close a parent popover.\n      event.stopPropagation();\n      handler();\n      return;\n    }\n    if (event.key === \"Enter\" || event.key === \" \") {\n      event.preventDefault();\n      event.stopPropagation();\n      if (!(min && focusedDay < min) && !(max && focusedDay > max)) selectDay(focusedDay);\n    }\n  };\n\n  const readout = (() => {\n    if (mode === \"single\") return value.from ? formatDayLong(value.from) : \"Pick a date\";\n    if (!value.from) return \"Pick a start date\";\n    if (!value.to) return \"Now pick an end date\";\n    const days = rangeLengthDays(value.from, value.to);\n    return `${days} day${days === 1 ? \"\" : \"s\"} selected`;\n  })();\n\n  const panelMotion = {\n    initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: -6, filter: \"blur(4px)\" },\n    animate: reduceMotion\n      ? { opacity: 1, transition: { duration: 0.15 } }\n      : {\n          opacity: 1,\n          scale: 1,\n          y: 0,\n          filter: \"blur(0px)\",\n          transition: { duration: 0.22, ease: EASE },\n        },\n    exit: reduceMotion\n      ? { opacity: 0, transition: { duration: 0.1 } }\n      : {\n          opacity: 0,\n          scale: 0.96,\n          y: -4,\n          filter: \"blur(3px)\",\n          transition: { duration: 0.18, ease: COLLAPSE_EASE },\n        },\n  };\n\n  const headerButton = (open: boolean) =>\n    cn(\n      \"flex cursor-pointer items-center gap-1 rounded-lg py-1 text-[14px] font-bold tracking-tight text-ink transition-colors focus-visible:outline-2 focus-visible:outline-brand\",\n      open ? \"bg-brand/15\" : \"hover:bg-paper\",\n    );\n\n  const chevron = (open: boolean) => (\n    <ChevronDown\n      size={13}\n      strokeWidth={2.5}\n      className={cn(\n        \"opacity-45 transition-transform duration-200\",\n        open && \"rotate-180 opacity-100\",\n      )}\n    />\n  );\n\n  const panelHeader = (label: string, close: string) => (\n    <div className=\"mb-2 flex items-center justify-between border-b border-rule-soft pb-1.5\">\n      <span className=\"text-[11px] font-bold uppercase tracking-wider text-dim\">{label}</span>\n      <button\n        type=\"button\"\n        aria-label={close}\n        onClick={() => setActiveDropdown(null)}\n        className=\"grid h-5 w-5 place-items-center rounded text-faint hover:text-ink\"\n      >\n        <X size={12} strokeWidth={2.5} />\n      </button>\n    </div>\n  );\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative font-body\", className)}\n      onMouseLeave={() => setHover(null)}\n    >\n      {/* Month and year header */}\n      <div className=\"flex items-center justify-between gap-1 px-0.5\">\n        <button\n          type=\"button\"\n          aria-label={`Previous month, ${monthLabel(previousMonth)}`}\n          disabled={!canGoBack}\n          onClick={() => {\n            setMonth(previousMonth);\n            setActiveDropdown(null);\n          }}\n          className={NAV_BUTTON}\n        >\n          <ChevronLeft size={16} strokeWidth={2.5} />\n        </button>\n\n        <div className=\"flex items-center gap-1\">\n          <button\n            type=\"button\"\n            aria-haspopup=\"dialog\"\n            aria-expanded={activeDropdown === \"month\"}\n            aria-label={`Select month, currently ${currentMonthName}`}\n            onClick={() => setActiveDropdown((curr) => (curr === \"month\" ? null : \"month\"))}\n            className={cn(headerButton(activeDropdown === \"month\"), \"px-2.5\")}\n          >\n            <span>{currentMonthName}</span>\n            {chevron(activeDropdown === \"month\")}\n          </button>\n          <button\n            type=\"button\"\n            aria-haspopup=\"dialog\"\n            aria-expanded={activeDropdown === \"year\"}\n            aria-label={`Select year, currently ${currentYear}`}\n            onClick={() => {\n              setYearSearch(\"\");\n              setActiveDropdown((curr) => (curr === \"year\" ? null : \"year\"));\n            }}\n            className={cn(headerButton(activeDropdown === \"year\"), \"px-2 tabular-nums\")}\n          >\n            <span>{currentYear}</span>\n            {chevron(activeDropdown === \"year\")}\n          </button>\n        </div>\n\n        <button\n          type=\"button\"\n          aria-label={`Next month, ${monthLabel(nextMonth)}`}\n          disabled={!canGoForward}\n          onClick={() => {\n            setMonth(nextMonth);\n            setActiveDropdown(null);\n          }}\n          className={NAV_BUTTON}\n        >\n          <ChevronRight size={16} strokeWidth={2.5} />\n        </button>\n      </div>\n\n      {/* Month and year panels */}\n      <AnimatePresence>\n        {activeDropdown === \"month\" && (\n          <motion.div key=\"month\" {...panelMotion} ref={keepInViewport} className={PANEL}>\n            {panelHeader(`Select month (${currentYear})`, \"Close month dropdown\")}\n            <div className=\"grid grid-cols-2 gap-1\">\n              {MONTH_NAMES.map((name, index) => {\n                const isSelected = index === currentMonthIndex;\n                const key = `${currentYear}-${String(index + 1).padStart(2, \"0\")}`;\n                const isDisabled = Boolean(\n                  (min && `${key}-31` < min) || (max && `${key}-01` > max),\n                );\n                return (\n                  <button\n                    key={name}\n                    type=\"button\"\n                    disabled={isDisabled}\n                    onClick={() => jumpTo(key)}\n                    className={cn(\n                      \"flex items-center justify-between rounded-lg px-2.5 py-1.5 text-left text-xs font-semibold transition-colors\",\n                      isSelected ? \"bg-brand font-bold text-on-solid\" : \"text-ink hover:bg-paper\",\n                      isDisabled && \"pointer-events-none opacity-25\",\n                    )}\n                  >\n                    <span>{name}</span>\n                    {isSelected && <Check size={12} strokeWidth={3} />}\n                  </button>\n                );\n              })}\n            </div>\n          </motion.div>\n        )}\n\n        {activeDropdown === \"year\" && (\n          <motion.div key=\"year\" {...panelMotion} ref={keepInViewport} className={PANEL}>\n            {panelHeader(\"Select or type year\", \"Close year dropdown\")}\n\n            <form onSubmit={handleYearSearchSubmit} className=\"mb-2 flex items-center gap-1.5\">\n              <div className=\"relative flex-1\">\n                <input\n                  ref={yearInputRef}\n                  type=\"text\"\n                  inputMode=\"numeric\"\n                  aria-label=\"Year\"\n                  placeholder=\"Type year (e.g. 2021)…\"\n                  value={yearSearch}\n                  onChange={(event) =>\n                    setYearSearch(event.target.value.replace(/[^0-9]/g, \"\").slice(0, 4))\n                  }\n                  className=\"w-full rounded-md border border-rule bg-card px-2.5 py-1 font-mono text-xs text-ink placeholder:text-faint focus:border-brand focus:outline-none\"\n                />\n                {yearSearch && (\n                  <button\n                    type=\"button\"\n                    aria-label=\"Clear year\"\n                    onClick={() => setYearSearch(\"\")}\n                    className=\"absolute right-1.5 top-1/2 -translate-y-1/2 text-faint hover:text-ink\"\n                  >\n                    <X size={11} />\n                  </button>\n                )}\n              </div>\n              <button\n                type=\"submit\"\n                disabled={!yearSearch.trim()}\n                className=\"rounded-md bg-brand px-2.5 py-1 text-xs font-bold text-on-solid transition-opacity hover:opacity-90 disabled:opacity-40\"\n              >\n                Jump\n              </button>\n            </form>\n\n            <div className=\"grid max-h-44 grid-cols-3 gap-1 overflow-y-auto pr-1\">\n              {filteredYears.map((y) => {\n                const isSelected = y === currentYear;\n                return (\n                  <button\n                    key={y}\n                    ref={isSelected ? activeYearRef : undefined}\n                    type=\"button\"\n                    onClick={() =>\n                      jumpTo(`${y}-${String(currentMonthIndex + 1).padStart(2, \"0\")}`)\n                    }\n                    className={cn(\n                      \"rounded-md py-1.5 text-center font-mono text-xs transition-colors\",\n                      isSelected ? \"bg-brand font-bold text-on-solid\" : \"text-ink hover:bg-paper\",\n                    )}\n                  >\n                    {y}\n                  </button>\n                );\n              })}\n              {filteredYears.length === 0 && (\n                <p className=\"col-span-3 py-3 text-center text-xs text-faint\">No matching year</p>\n              )}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      <div className=\"mt-2 grid grid-cols-7 gap-y-0.5\">\n        {WEEKDAY_LABELS.map((label, index) => (\n          <span\n            key={`${label}-${index}`}\n            aria-hidden=\"true\"\n            className={cn(\n              CELL,\n              \"grid place-items-center text-[10px] font-bold uppercase tracking-[0.06em] text-faint\",\n            )}\n          >\n            {label}\n          </span>\n        ))}\n      </div>\n\n      <div\n        ref={gridRef}\n        role=\"grid\"\n        aria-label={`${monthLabel(month)}. Use the arrow keys to move by day, Page Up and Page Down by month.`}\n        onKeyDown={onGridKeyDown}\n      >\n        <motion.div\n          key={month}\n          role=\"rowgroup\"\n          initial={reduceMotion ? false : { opacity: 0.5, filter: \"blur(2px)\" }}\n          animate={{ opacity: 1, filter: \"blur(0px)\" }}\n          transition={{ duration: 0.16, ease: EASE }}\n        >\n          {weeks.map((week) => (\n            <div key={week[0].iso} role=\"row\" className=\"grid grid-cols-7 gap-y-0.5\">\n              {week.map((cell) => {\n                const flags = dayFlags(cell.iso, { selection: value, hover, min, max, today });\n\n                // The track sits behind the day: full width inside the span,\n                // half width under each end, so consecutive cells join into one\n                // bar while the caps keep their own pill. A preview is lighter.\n                const track = flags.isPreview ? \"bg-brand/[0.10]\" : \"bg-brand/[0.16]\";\n                const isCap = flags.isStart || flags.isEnd;\n                const oneDay = flags.isStart && flags.isEnd;\n\n                return (\n                  <div\n                    key={cell.iso}\n                    role=\"gridcell\"\n                    aria-selected={isCap}\n                    className=\"relative\"\n                  >\n                    {flags.isInside && (\n                      <span aria-hidden=\"true\" className={cn(\"absolute inset-0\", track)} />\n                    )}\n                    {flags.isStart && !oneDay && (\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\"absolute inset-y-0 right-0 w-1/2\", track)}\n                      />\n                    )}\n                    {flags.isEnd && !oneDay && (\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\"absolute inset-y-0 left-0 w-1/2\", track)}\n                      />\n                    )}\n\n                    <button\n                      type=\"button\"\n                      data-day={cell.iso}\n                      tabIndex={cell.iso === focusedDay ? 0 : -1}\n                      disabled={flags.isDisabled}\n                      aria-current={flags.isToday ? \"date\" : undefined}\n                      aria-label={formatDayLong(cell.iso)}\n                      onMouseEnter={() => setHover(cell.iso)}\n                      onFocus={() => setHover(cell.iso)}\n                      onClick={() => selectDay(cell.iso)}\n                      className={cn(\n                        CELL,\n                        \"relative z-10 grid place-items-center rounded-lg text-[12px] tabular-nums transition-colors\",\n                        \"focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-brand\",\n                        flags.isDisabled ? \"cursor-not-allowed text-faint\" : \"hover:bg-paper\",\n                        isCap\n                          ? \"bg-brand font-bold text-on-solid hover:bg-brand\"\n                          : cell.inMonth\n                            ? \"font-medium text-ink\"\n                            : \"font-medium text-faint\",\n                        flags.isToday && !isCap && \"ring-1 ring-inset ring-brand/45\",\n                      )}\n                    >\n                      {cell.dayOfMonth}\n                    </button>\n                  </div>\n                );\n              })}\n            </div>\n          ))}\n        </motion.div>\n      </div>\n\n      <p className=\"mt-2 text-center text-[11px] font-medium text-faint\" aria-live=\"polite\">\n        {readout}\n      </p>\n\n      {presets.length > 0 && (\n        <div className=\"mt-2 flex flex-wrap gap-1 pt-2\">\n          {presets.map((preset) => {\n            const isActive = value.from === preset.from && value.to === preset.to;\n            return (\n              <button\n                key={preset.label}\n                type=\"button\"\n                aria-pressed={isActive}\n                onClick={() => {\n                  onChange({ from: preset.from, to: preset.to });\n                  setMonth(monthKeyOf(preset.from));\n                  setFocusedDay(preset.from);\n                }}\n                className={cn(\n                  \"rounded-full px-2.5 py-1 text-[11px] font-bold transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand\",\n                  isActive\n                    ? \"bg-brand/15 text-ink\"\n                    : \"bg-paper text-dim hover:bg-paper-sunk hover:text-ink\",\n                )}\n              >\n                {preset.label}\n              </button>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default DateRangeCalendar;\n",
      "type": "registry:component"
    },
    {
      "path": "registry/items/date-range-calendar/date-range-picker.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { CalendarRange, Check, ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotionConfig } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  DateRangeCalendar,\n  isCompleteRange,\n  keepInViewport,\n  type CalendarPreset,\n  type RangeSelection,\n} from \"./date-range-calendar\";\n\n/**\n * A period dropdown: a short list of presets (\"Past 30 days\") with an optional\n * \"Custom range\" step that widens the same popover into a range calendar. A\n * bespoke window never leaves the control the user already learned; the\n * dropdown just grows a second step.\n */\n\n/**\n * A selectable window. Two shapes:\n * - `points`: how many trailing points to keep; omit for \"everything\" (presets).\n * - `from`/`to`: inclusive ISO days (custom ranges). These win over `points`.\n */\nexport type PeriodOption = { label: string; points?: number; from?: string; to?: string };\n\n/**\n * The custom-range editor's own width, fixed on purpose. The popover\n * shrink-wraps its content, and the calendar's cells are `w-full`, so \"auto\"\n * would be circular; pinning the editor gives the width animation a number.\n * 16.5rem puts each day column at ~35px.\n */\nconst RANGE_EDITOR_WIDTH = \"16.5rem\";\n\n/* ─── Motion ────────────────────────────────────────────────────────────────\n * Opening and closing are deliberately not symmetrical. EASE is front-loaded:\n * on a press the box is most of the way open in the first third. Run backwards\n * it lurches away and crawls the last 20%, so closing uses its mirror\n * (COLLAPSE_EASE) over a slightly longer duration. The calendar fades in\n * behind the box as it opens, and out ahead of it as it closes.\n */\nconst EASE = [0.2, 0.7, 0.2, 1] as const;\nconst COLLAPSE_EASE = [0.8, 0, 0.3, 0.8] as const;\nconst EXPAND = { duration: 0.42, ease: EASE } as const;\nconst COLLAPSE = { duration: 0.52, ease: COLLAPSE_EASE } as const;\nconst FADE_IN = { duration: 0.28, delay: 0.08, ease: EASE } as const;\nconst FADE_OUT = { duration: 0.3, ease: COLLAPSE_EASE } as const;\nconst POPOVER_IN = { duration: 0.16, ease: EASE } as const;\nconst POPOVER_OUT = { duration: 0.26, ease: COLLAPSE_EASE } as const;\nconst INSTANT = { duration: 0 } as const;\n\nconst SHORT_MONTHS = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n] as const;\n\n/**\n * \"12 Aug – 25 Aug\", appending the year whenever either end isn't this year.\n * Month names are spelled out here, not by Intl: runtimes disagree on \"Sep\".\n */\nexport function formatRangeLabel(from: string, to: string, now: Date = new Date()): string {\n  const thisYear = now.getUTCFullYear();\n  const label = (iso: string) => {\n    const d = new Date(`${iso}T00:00:00Z`);\n    const year = d.getUTCFullYear();\n    return `${d.getUTCDate()} ${SHORT_MONTHS[d.getUTCMonth()]}${year === thisYear ? \"\" : ` ${year}`}`;\n  };\n  return `${label(from)} – ${label(to)}`;\n}\n\nfunction CustomRangeEditor({\n  initial,\n  canReset,\n  min,\n  max,\n  now,\n  presetOptions,\n  onApply,\n  onReset,\n}: {\n  initial: { from: string; to: string } | null;\n  canReset: boolean;\n  min?: string | null;\n  max?: string | null;\n  now?: Date;\n  presetOptions?: CalendarPreset[];\n  onApply: (from: string, to: string) => void;\n  onReset: () => void;\n}) {\n  const [selection, setSelection] = useState<RangeSelection>(() => ({\n    from: initial?.from ?? null,\n    to: initial?.to ?? null,\n  }));\n  const valid = isCompleteRange(selection);\n\n  return (\n    <form\n      className=\"mt-1 space-y-2 rounded-xl bg-paper p-2\"\n      style={{ width: RANGE_EDITOR_WIDTH }}\n      onSubmit={(event) => {\n        event.preventDefault();\n        if (isCompleteRange(selection)) onApply(selection.from, selection.to);\n      }}\n    >\n      <DateRangeCalendar\n        value={selection}\n        onChange={setSelection}\n        min={min}\n        max={max}\n        now={now}\n        presetOptions={presetOptions}\n      />\n\n      <div className=\"flex items-center gap-1.5\">\n        <button\n          type=\"submit\"\n          disabled={!valid}\n          className=\"flex-1 rounded-lg bg-brand px-3 py-1.5 text-[12px] font-bold text-on-solid transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand disabled:pointer-events-none disabled:opacity-35\"\n        >\n          Apply range\n        </button>\n        {canReset && (\n          <button\n            type=\"button\"\n            aria-label=\"Reset to default period\"\n            onClick={onReset}\n            className=\"rounded-lg px-3 py-1.5 text-[12px] font-bold text-dim transition-colors hover:bg-paper-sunk hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand\"\n          >\n            Reset\n          </button>\n        )}\n      </div>\n    </form>\n  );\n}\n\nexport type DateRangePickerProps = {\n  /** Label of the selected option; shown on the trigger. */\n  value: string;\n  options: PeriodOption[];\n  onChange: (option: PeriodOption) => void;\n  /** Adds a \"Custom range\" step for picking exact dates on a calendar. */\n  allowCustomRange?: boolean;\n  /** Where \"Reset\" sends the selection back to (usually the default preset). */\n  defaultOption?: PeriodOption;\n  /**\n   * Inclusive ISO-day bounds the calendar may select within, normally the first\n   * and last day the data holds. Without them a user can pick a window the data\n   * does not cover and get an empty chart, which reads as broken.\n   */\n  rangeMin?: string | null;\n  rangeMax?: string | null;\n  /** Shortcuts under the calendar. Defaults to This month / Last month / This quarter. */\n  calendarPresets?: CalendarPreset[];\n  /** Fixes \"today\" for the calendar and the label's year; defaults to the clock. */\n  now?: Date;\n  className?: string;\n};\n\nexport function DateRangePicker({\n  value,\n  options,\n  onChange,\n  allowCustomRange = false,\n  defaultOption,\n  rangeMin,\n  rangeMax,\n  calendarPresets,\n  now,\n  className,\n}: DateRangePickerProps) {\n  const reduceMotion = useReducedMotionConfig();\n  const [open, setOpen] = useState(false);\n  const [customOpen, setCustomOpen] = useState(false);\n  const [appliedCustom, setAppliedCustom] = useState<{\n    label: string;\n    from: string;\n    to: string;\n  } | null>(null);\n  const isCustomSelected = appliedCustom !== null && value === appliedCustom.label;\n\n  const optionClass = (selected: boolean) =>\n    cn(\n      \"flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-[13px] font-medium transition-colors\",\n      selected ? \"bg-paper text-ink\" : \"text-dim hover:bg-paper hover:text-ink\",\n    );\n\n  return (\n    // Below `sm` the wrapper takes the full row width, so the right-anchored\n    // panel drops from the row's right edge instead of spilling off-screen\n    // from a trigger that wrapped to the left.\n    <div\n      className={cn(\"relative font-body max-sm:w-full\", open && \"z-50\", className)}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setOpen(false);\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") setOpen(false);\n      }}\n    >\n      <button\n        type=\"button\"\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        onClick={() => setOpen((previous) => !previous)}\n        className=\"flex items-center gap-1.5 rounded-full bg-paper px-2.5 py-1 text-[13px] font-medium text-dim transition-colors hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand max-sm:inline-flex\"\n      >\n        <span>{value}</span>\n        <motion.span\n          className=\"block\"\n          animate={{ rotate: open ? 180 : 0 }}\n          transition={reduceMotion ? INSTANT : { duration: 0.2 }}\n        >\n          <ChevronDown size={14} strokeWidth={2.5} />\n        </motion.span>\n      </button>\n\n      <AnimatePresence>\n        {open && (\n          <motion.ul\n            role=\"listbox\"\n            initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: -4 }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            exit={\n              reduceMotion\n                ? { opacity: 0, transition: { duration: 0.1 } }\n                : { opacity: 0, scale: 0.95, y: -4, transition: POPOVER_OUT }\n            }\n            transition={POPOVER_IN}\n            /*\n             * `w-max` is what lets the widening animate: the popover's width is\n             * whatever its widest child measures, so it follows the editor's own\n             * animated width frame by frame. It grows leftwards from `right-0`;\n             * the max-width and keepInViewport keep it on a narrow phone.\n             */\n            ref={keepInViewport}\n            className=\"absolute right-0 top-full z-50 mt-1.5 w-max min-w-[10.5rem] max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-rule bg-popover/95 p-1.5 shadow-[0_10px_30px_color-mix(in_oklab,var(--ink)_15%,transparent)] backdrop-blur-md\"\n          >\n            {options.map((option) => {\n              const isSelected = option.label === value;\n              return (\n                <li key={option.label}>\n                  <button\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    onClick={() => {\n                      onChange(option);\n                      setCustomOpen(false);\n                      setOpen(false);\n                    }}\n                    className={optionClass(isSelected)}\n                  >\n                    <span>{option.label}</span>\n                    {isSelected && (\n                      <motion.span\n                        initial={reduceMotion ? false : { scale: 0 }}\n                        animate={{ scale: 1 }}\n                        transition={{ type: \"spring\", stiffness: 500, damping: 25 }}\n                      >\n                        <Check size={14} strokeWidth={3} />\n                      </motion.span>\n                    )}\n                  </button>\n                </li>\n              );\n            })}\n\n            {allowCustomRange && (\n              <>\n                <li aria-hidden=\"true\" className=\"mx-2 my-1 border-t border-rule\" />\n                <li>\n                  <button\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={isCustomSelected}\n                    onClick={() => setCustomOpen((previous) => !previous)}\n                    className={optionClass(isCustomSelected)}\n                  >\n                    <span>{appliedCustom?.label ?? \"Custom range\"}</span>\n                    <motion.span\n                      initial={false}\n                      animate={{ rotate: customOpen ? 90 : 0 }}\n                      transition={reduceMotion ? INSTANT : { duration: 0.2 }}\n                      className=\"shrink-0\"\n                    >\n                      {isCustomSelected ? (\n                        <Check size={14} strokeWidth={3} />\n                      ) : (\n                        <CalendarRange size={14} strokeWidth={2.5} />\n                      )}\n                    </motion.span>\n                  </button>\n                  <AnimatePresence initial={false}>\n                    {customOpen && (\n                      <motion.div\n                        /*\n                         * Width AND height on the one curve: growing the\n                         * editor's own width from 0 makes the shrink-wrapped\n                         * popover widen with it, so the box opens as a single\n                         * motion. The opening fade delay must not apply on\n                         * exit, or the calendar would still show while the box\n                         * narrows past it.\n                         */\n                        initial={{ opacity: 0, width: 0, height: 0 }}\n                        animate={{ opacity: 1, width: RANGE_EDITOR_WIDTH, height: \"auto\" }}\n                        exit={{\n                          opacity: 0,\n                          width: 0,\n                          height: 0,\n                          transition: reduceMotion\n                            ? INSTANT\n                            : { width: COLLAPSE, height: COLLAPSE, opacity: FADE_OUT },\n                        }}\n                        transition={\n                          reduceMotion\n                            ? INSTANT\n                            : { width: EXPAND, height: EXPAND, opacity: FADE_IN }\n                        }\n                        className=\"overflow-hidden\"\n                      >\n                        <CustomRangeEditor\n                          initial={appliedCustom}\n                          canReset={isCustomSelected}\n                          min={rangeMin}\n                          max={rangeMax}\n                          now={now}\n                          presetOptions={calendarPresets}\n                          onReset={() => {\n                            setAppliedCustom(null);\n                            setCustomOpen(false);\n                            setOpen(false);\n                            if (defaultOption) onChange(defaultOption);\n                          }}\n                          onApply={(from, to) => {\n                            const label = formatRangeLabel(from, to, now);\n                            setAppliedCustom({ label, from, to });\n                            onChange({ label, from, to });\n                            setCustomOpen(false);\n                            setOpen(false);\n                          }}\n                        />\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </li>\n              </>\n            )}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default DateRangePicker;\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "kind": "widget",
    "whenToUse": "Use for picking a reporting window or a single due date inside a card or popover, especially where a native date input would close its parent on blur or format to the wrong locale. Use DateRangePicker when presets cover most needs and exact dates are the exception; not for date-and-time entry or recurring schedules.",
    "origin": {
      "project": "180Connect",
      "path": "src/components/ui/date-range-calendar.tsx (rules from src/lib/date-range.ts); popover from PeriodSelect in src/components/ui/metric-controls.tsx"
    },
    "credits": []
  },
  "type": "registry:component"
}
