{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"date-range-picker","type":"registry:component","title":"Date range picker","description":"A range is one liquid band: it follows your pointer between days, presets flow it to their span, and the dates morph as it moves.","author":"Hairline UI","dependencies":["lucide-react"],"registryDependencies":["https://hairlineui.com/r/number-roll.json","https://hairlineui.com/r/text-morph.json"],"files":[{"path":"registry/date-range-picker.tsx","type":"registry:component","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { CalendarDays, ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { NumberRoll } from \"./number-roll\";\nimport { TextMorph } from \"./text-morph\";\n\n/* ─────────────────────────────────────────────────────────\n * DATE RANGE PICKER: the range is one liquid band\n *\n *   open      the panel unfolds out of the pill's footprint:\n *             presets on one side, the month on the other\n *   pick      the first day you tap drops a dot; a band follows\n *             the pointer to wherever you'd end it, and a second\n *             dot rides along with it. The second tap sets it.\n *             The band is one shape per week, stretching between\n *             days, and the dots glide from day to day, so\n *             nothing lights up cell by cell\n *   presets   a highlight slides to the preset you choose; the\n *             band flows to its span and the label and the day\n *             count morph and roll to match\n *   month     ‹ and › slide the next month in from the side you\n *             went\n *   apply     the pill's label morphs to the new range; Cancel,\n *             Escape or a tap outside folds the panel back into\n *             the pill unchanged\n *\n * The days are a grid for the keyboard too: arrows, Home/End,\n * Page Up/Down for months, Enter to pick.\n * ───────────────────────────────────────────────────────── */\n\nexport interface DateRange {\n  start: Date;\n  end: Date;\n}\n\nexport interface DateRangePreset {\n  label: string;\n  range: (today: Date) => DateRange;\n}\n\nexport interface DateRangePickerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\" | \"defaultValue\"> {\n  value?: DateRange | null;\n  defaultValue?: DateRange | null;\n  onValueChange?: (range: DateRange) => void;\n  /** Shortcuts down the side; pass [] for none. */\n  presets?: DateRangePreset[];\n  /** Days before or after these can't be picked. */\n  min?: Date;\n  max?: Date;\n  /** Count days (analytics) or nights (stays, which need at least one). */\n  unit?: \"days\" | \"nights\";\n  /** Weeks start on Monday (1) or Sunday (0). */\n  weekStartsOn?: 0 | 1;\n  locales?: string | string[];\n  placeholder?: string;\n  /** Line the panel up with the pill's left or right edge. */\n  align?: \"start\" | \"end\";\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}\n\nconst EASE = \"cubic-bezier(0.16,1,0.3,1)\";\nconst THROW = \"cubic-bezier(0.34,1.36,0.64,1)\";\nconst FOCUS = \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40\";\nconst GLIDE = 340;\n/** The gap between a day's dot and its cell, in CSS. */\nconst INSET = \"calc((var(--cell) - var(--dot)) / 2)\";\n\nconst day = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst monthOf = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);\nconst same = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\nconst order = (a: Date, b: Date): DateRange => (a <= b ? { start: day(a), end: day(b) } : { start: day(b), end: day(a) });\nconst span = (r: DateRange) => Math.round((day(r.end).getTime() - day(r.start).getTime()) / 86_400_000) + 1;\n/** The same day of the month n months on, kept inside that month. */\nconst shiftMonth = (d: Date, n: number) => {\n  const last = new Date(d.getFullYear(), d.getMonth() + n + 1, 0).getDate();\n  return new Date(d.getFullYear(), d.getMonth() + n, Math.min(d.getDate(), last));\n};\n\nexport const DATE_RANGE_PRESETS: DateRangePreset[] = [\n  { label: \"Today\", range: (t) => ({ start: t, end: t }) },\n  { label: \"Last 7 days\", range: (t) => ({ start: addDays(t, -6), end: t }) },\n  { label: \"Last 30 days\", range: (t) => ({ start: addDays(t, -29), end: t }) },\n  { label: \"This month\", range: (t) => ({ start: new Date(t.getFullYear(), t.getMonth(), 1), end: t }) },\n  { label: \"Last month\", range: (t) => ({ start: new Date(t.getFullYear(), t.getMonth() - 1, 1), end: new Date(t.getFullYear(), t.getMonth(), 0) }) },\n  { label: \"This year\", range: (t) => ({ start: new Date(t.getFullYear(), 0, 1), end: t }) },\n];\n\nconst reducedQuery = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(reducedQuery);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\nconst useReducedMotion = () =>\n  React.useSyncExternalStore(subscribeReduced, () => window.matchMedia(reducedQuery).matches, () => false);\n\n/** Six weeks of days for the month, starting on the week's first day. */\nfunction weeksOf(month: Date, weekStartsOn: number) {\n  const lead = (month.getDay() - weekStartsOn + 7) % 7;\n  const start = addDays(month, -lead);\n  return Array.from({ length: 6 }, (_, w) => Array.from({ length: 7 }, (_, d) => addDays(start, w * 7 + d)));\n}\n\n/** The dot on the range's first or last day; it glides when the day changes and pops in when it appears. */\nfunction Dot({ row, col, reduced, light }: { row: number; col: number; reduced: boolean; light: boolean }) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  React.useEffect(() => {\n    if (reduced) return;\n    ref.current?.animate([{ transform: \"scale(0.5)\", opacity: 0 }, { transform: \"scale(1)\", opacity: 1 }], { duration: 300, easing: THROW });\n  }, [reduced]);\n  return (\n    <span\n      ref={ref}\n      aria-hidden=\"true\"\n      className={`absolute rounded-full ${light ? \"bg-primary/80\" : \"bg-primary\"}`}\n      style={{\n        left: `calc(var(--cell) * ${col} + ${INSET})`,\n        top: `calc(var(--cell) * ${row} + ${INSET})`,\n        width: \"var(--dot)\",\n        height: \"var(--dot)\",\n        transition: reduced ? \"none\" : `left ${GLIDE}ms ${EASE}, top ${GLIDE}ms ${EASE}, background-color 200ms`,\n      }}\n    />\n  );\n}\n\nexport function DateRangePicker({\n  value,\n  defaultValue = null,\n  onValueChange,\n  presets = DATE_RANGE_PRESETS,\n  min,\n  max,\n  unit = \"days\",\n  weekStartsOn = 1,\n  locales,\n  placeholder = \"Pick dates\",\n  align = \"start\",\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  className = \"\",\n  ...props\n}: DateRangePickerProps) {\n  const reduced = useReducedMotion();\n  const id = React.useId();\n  const [today] = React.useState(() => day(new Date()));\n  const [own, setOwn] = React.useState<DateRange | null>(defaultValue);\n  const committed = value === undefined ? own : value;\n  const [phase, setPhase] = React.useState<\"closed\" | \"opening\" | \"open\" | \"closing\">(() => ((openProp ?? defaultOpen) ? \"open\" : \"closed\"));\n  const [draft, setDraft] = React.useState<DateRange | null>(() => committed && order(committed.start, committed.end));\n  const [anchor, setAnchor] = React.useState<Date | null>(null);\n  const [hover, setHover] = React.useState<Date | null>(null);\n  const [month, setMonth] = React.useState(() => monthOf(committed?.end ?? today));\n  const [focusDay, setFocusDay] = React.useState<Date>(() => day(committed?.end ?? today));\n  const [frame, setFrame] = React.useState({ shift: 0, left: 0, right: 0, max: 0 });\n  const [mark, setMark] = React.useState({ x: 0, y: 0, w: 0, h: 0, on: false, slide: false });\n  const [announce, setAnnounce] = React.useState(\"\");\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  const gridRef = React.useRef<HTMLDivElement>(null);\n  const presetRefs = React.useRef<(HTMLButtonElement | null)[]>([]);\n  const dayRefs = React.useRef(new Map<string, HTMLButtonElement>());\n  const focusGrid = React.useRef(false);\n  const isOpen = phase === \"open\" || phase === \"opening\";\n  const mounted = phase !== \"closed\";\n  const nights = unit === \"nights\";\n  const lo = min ? day(min) : null;\n  const hi = max ? day(max) : null;\n  const blocked = (d: Date) => Boolean((lo && d < lo) || (hi && d > hi));\n\n  const fmt = React.useMemo(() => new Intl.DateTimeFormat(locales ?? \"en-GB\", { day: \"numeric\", month: \"short\", year: \"numeric\" }), [locales]);\n  const longFmt = React.useMemo(() => new Intl.DateTimeFormat(locales ?? \"en-GB\", { weekday: \"long\", day: \"numeric\", month: \"long\", year: \"numeric\" }), [locales]);\n  const label = (r: DateRange | null) => (r ? (same(r.start, r.end) ? fmt.format(r.start) : fmt.formatRange(r.start, r.end)) : placeholder);\n  const monthTitle = new Intl.DateTimeFormat(locales ?? \"en-GB\", { month: \"long\", year: \"numeric\" }).format(month);\n  const weekdays = React.useMemo(() => {\n    const narrow = new Intl.DateTimeFormat(locales ?? \"en-GB\", { weekday: \"narrow\" });\n    return Array.from({ length: 7 }, (_, i) => narrow.format(new Date(2024, 0, 7 + i + weekStartsOn))); // 7 Jan 2024 was a Sunday\n  }, [locales, weekStartsOn]);\n  const weeks = React.useMemo(() => weeksOf(month, weekStartsOn), [month, weekStartsOn]);\n\n  const begin = () => {\n    setDraft(committed && order(committed.start, committed.end));\n    setAnchor(null);\n    setHover(null);\n    setMonth(monthOf(committed?.end ?? today));\n    setFocusDay(day(committed?.end ?? today));\n    setPhase(\"opening\");\n  };\n  const end = () => setPhase((p) => (p === \"closed\" ? p : \"closing\"));\n  const request = (next: boolean, returnFocus = true) => {\n    if (openProp === undefined) (next ? begin : end)();\n    onOpenChange?.(next);\n    if (!next && returnFocus) triggerRef.current?.focus();\n  };\n\n  // Controlled: follow the open prop.\n  React.useEffect(() => {\n    if (openProp === undefined) return;\n    if (openProp && !isOpen) begin();\n    if (!openProp && isOpen) end();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [openProp]);\n\n  // Before the panel shows: keep it inside the screen and any box that clips it (narrowing it if it\n  // has to), and find the pill's footprint inside it to unfold from.\n  const place = React.useCallback(() => {\n    const panel = panelRef.current;\n    const trigger = triggerRef.current;\n    if (!panel || !trigger) return;\n    let lo = 0;\n    let hi = window.innerWidth;\n    for (let el = rootRef.current?.parentElement; el && el !== document.body; el = el.parentElement) {\n      if (getComputedStyle(el).overflowX === \"visible\") continue;\n      const r = el.getBoundingClientRect();\n      lo = Math.max(lo, r.left);\n      hi = Math.min(hi, r.right);\n    }\n    const edge = 8;\n    const max = Math.max(240, hi - lo - edge * 2);\n    panel.style.maxWidth = `${max}px`;\n    setFrame((f) => {\n      const p = panel.getBoundingClientRect();\n      const left = p.left - f.shift;\n      let shift = 0;\n      if (left + p.width > hi - edge) shift = hi - edge - p.width - left;\n      if (left + shift < lo + edge) shift = lo + edge - left;\n      const t = trigger.getBoundingClientRect();\n      return { shift, max, left: Math.max(0, t.left - (left + shift)), right: Math.max(0, left + shift + p.width - t.right) };\n    });\n  }, []);\n  React.useLayoutEffect(() => {\n    if (phase === \"opening\" || (phase === \"open\" && frame.left === 0 && frame.right === 0)) place();\n  }, [phase, place, frame.left, frame.right]);\n  React.useEffect(() => {\n    if (!mounted) return;\n    window.addEventListener(\"resize\", place);\n    return () => window.removeEventListener(\"resize\", place);\n  }, [mounted, place]);\n\n  // Opening grows the panel on the next frame; closing folds it back, then unmounts it.\n  React.useEffect(() => {\n    if (phase === \"opening\") {\n      let inner = 0;\n      const outer = requestAnimationFrame(() => (inner = requestAnimationFrame(() => setPhase(\"open\"))));\n      return () => {\n        cancelAnimationFrame(outer);\n        cancelAnimationFrame(inner);\n      };\n    }\n    if (phase === \"closing\") {\n      const timer = window.setTimeout(() => setPhase(\"closed\"), reduced ? 0 : 240);\n      return () => window.clearTimeout(timer);\n    }\n  }, [phase, reduced]);\n\n  React.useEffect(() => {\n    if (!mounted) return;\n    const onDown = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) request(false, false);\n    };\n    window.addEventListener(\"pointerdown\", onDown);\n    return () => window.removeEventListener(\"pointerdown\", onDown);\n  });\n\n  // Keyboard moves (and opening from the pill) put focus on the day; clicks elsewhere leave it alone.\n  React.useEffect(() => {\n    if (phase !== \"open\" || !focusGrid.current) return;\n    focusGrid.current = false;\n    dayRefs.current.get(focusDay.toDateString())?.focus({ preventScroll: true });\n  }, [focusDay, phase, month]);\n\n  const showMonth = (target: Date) => {\n    const next = monthOf(target);\n    const dir = Math.sign(next.getTime() - month.getTime());\n    if (!dir) return;\n    setMonth(next);\n    if (reduced) return;\n    gridRef.current?.animate(\n      [\n        { transform: `translateX(${dir * 20}px)`, opacity: 0, filter: \"blur(4px)\" },\n        { transform: \"none\", opacity: 1, filter: \"blur(0px)\" },\n      ],\n      { duration: 380, easing: EASE },\n    );\n  };\n  const step = (n: number) => {\n    setFocusDay((d) => shiftMonth(d, n));\n    showMonth(new Date(month.getFullYear(), month.getMonth() + n, 1));\n  };\n\n  const pick = (d: Date) => {\n    if (blocked(d)) return;\n    if (!anchor) {\n      setAnchor(d);\n      setDraft({ start: d, end: d });\n      return;\n    }\n    if (nights && same(anchor, d)) return; // a stay needs at least one night\n    const r = order(anchor, d);\n    setDraft(r);\n    setAnchor(null);\n    setHover(null);\n    setAnnounce(label(r));\n  };\n\n  const choose = (p: DateRangePreset) => {\n    const r = p.range(today);\n    setDraft(r);\n    setAnchor(null);\n    setHover(null);\n    setFocusDay(day(r.end));\n    showMonth(r.end);\n    setAnnounce(label(r));\n  };\n\n  const apply = () => {\n    if (!draft) return;\n    if (value === undefined) setOwn(draft);\n    onValueChange?.(draft);\n    request(false);\n  };\n\n  // What the band shows: the range being picked (first day to pointer), otherwise the draft.\n  const band: DateRange | null = anchor && hover ? order(anchor, hover) : draft;\n  const count = band ? span(band) - (nights ? 1 : 0) : 0;\n  const ready = Boolean(draft) && !(nights && draft && same(draft.start, draft.end));\n  const presetIndex = presets.findIndex((p) => {\n    if (!draft || anchor) return false;\n    const r = p.range(today);\n    return same(r.start, draft.start) && same(r.end, draft.end);\n  });\n\n  // The preset highlight slides between presets; with none chosen it fades out where it is.\n  React.useLayoutEffect(() => {\n    if (!mounted) return;\n    const el = presetRefs.current[presetIndex];\n    setMark((m) =>\n      el\n        ? { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight, on: true, slide: m.on }\n        : { ...m, on: false, slide: false },\n    );\n    // In the narrow row of chips, scroll the chosen one into the middle.\n    const row = el?.parentElement;\n    if (el && row && row.scrollWidth > row.clientWidth)\n      row.scrollTo({ left: el.offsetLeft - (row.clientWidth - el.offsetWidth) / 2, behavior: phase === \"open\" && !reduced ? \"smooth\" : \"auto\" });\n  }, [presetIndex, mounted, phase, reduced]);\n\n  const tabDay = weeks.some((w) => w.some((d) => same(d, focusDay))) ? focusDay : month;\n  const onGridKey = (e: React.KeyboardEvent) => {\n    const col = (focusDay.getDay() - weekStartsOn + 7) % 7;\n    const moves: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(focusDay, -1),\n      ArrowRight: () => addDays(focusDay, 1),\n      ArrowUp: () => addDays(focusDay, -7),\n      ArrowDown: () => addDays(focusDay, 7),\n      Home: () => addDays(focusDay, -col),\n      End: () => addDays(focusDay, 6 - col),\n      PageUp: () => shiftMonth(focusDay, -1),\n      PageDown: () => shiftMonth(focusDay, 1),\n    };\n    if (!(e.key in moves)) return;\n    e.preventDefault();\n    const next = moves[e.key]();\n    focusGrid.current = true;\n    setFocusDay(next);\n    if (anchor) setHover(next);\n    showMonth(next);\n  };\n\n  const find = (d: Date) => {\n    for (let r = 0; r < 6; r++) for (let c = 0; c < 7; c++) if (same(weeks[r][c], d)) return { r, c };\n    return null;\n  };\n  const startAt = band ? find(band.start) : null;\n  const endAt = band ? find(band.end) : null;\n  const monthKey = `${month.getFullYear()}-${month.getMonth()}`;\n  const prevOff = Boolean(lo && month <= monthOf(lo));\n  const nextOff = Boolean(hi && month >= monthOf(hi));\n  const closedClip = `inset(0 ${frame.right}px calc(100% - 36px) ${frame.left}px round 18px)`;\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative inline-block ${className}`}\n      {...props}\n      onKeyDown={(e) => {\n        props.onKeyDown?.(e);\n        if (e.key !== \"Escape\" || !isOpen) return;\n        e.stopPropagation();\n        request(false);\n      }}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={isOpen}\n        aria-controls={mounted ? `${id}-panel` : undefined}\n        onClick={() => {\n          if (!isOpen) focusGrid.current = true;\n          request(!isOpen);\n        }}\n        className={`inline-flex h-9 max-w-full items-center gap-2 rounded-full border border-border pl-3 pr-2.5 text-[13px] font-medium text-foreground transition-colors hover:bg-accent ${isOpen ? \"bg-accent\" : \"bg-background\"} ${FOCUS}`}\n      >\n        <CalendarDays aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        <TextMorph className=\"truncate tabular-nums\">{label(committed)}</TextMorph>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className=\"size-3.5 shrink-0 text-muted-foreground\"\n          style={{ transform: isOpen ? \"rotate(180deg)\" : \"none\", transition: reduced ? \"none\" : `transform 320ms ${EASE}` }}\n        />\n      </button>\n\n      {mounted && (\n        <div\n          ref={panelRef}\n          id={`${id}-panel`}\n          role=\"dialog\"\n          aria-label=\"Choose dates\"\n          className={`@container absolute top-full z-50 mt-2 rounded-[20px] border border-border bg-popover text-popover-foreground ${align === \"end\" ? \"right-0\" : \"left-0\"} ${\n            presets.length ? \"w-[min(400px,calc(100vw-24px))]\" : \"w-[min(292px,calc(100vw-24px))]\"\n          }`}\n          style={{\n            translate: `${frame.shift}px 0`,\n            maxWidth: frame.max || undefined,\n            clipPath: phase === \"open\" ? \"inset(0 0 0 0 round 20px)\" : closedClip,\n            opacity: phase === \"open\" ? 1 : 0,\n            transition: reduced\n              ? \"none\"\n              : phase === \"closing\"\n                ? `clip-path 240ms ${EASE}, opacity 180ms ${EASE} 40ms`\n                : `clip-path 440ms ${EASE}, opacity 200ms ${EASE}`,\n          }}\n        >\n          <div\n            className={`grid p-2 [--cell:min(40px,calc((100cqw_-_26px)/7))] [--dot:calc(var(--cell)_-_6px)] ${\n              presets.length ? \"@min-[390px]:grid-cols-[112px_1fr] @min-[390px]:[--cell:36px]\" : \"\"\n            }`}\n          >\n            {presets.length > 0 && (\n              <div\n                role=\"group\"\n                aria-label=\"Presets\"\n                className=\"relative -mx-2 flex gap-1 overflow-x-auto px-2 pb-2 pr-8 [mask-image:linear-gradient(to_right,transparent,black_10px,black_calc(100%_-_32px),transparent)] [scrollbar-width:none] @min-[390px]:mx-0 @min-[390px]:flex-col @min-[390px]:overflow-visible @min-[390px]:border-r @min-[390px]:border-border @min-[390px]:px-0 @min-[390px]:pb-0 @min-[390px]:pr-2 @min-[390px]:[mask-image:none] [&::-webkit-scrollbar]:hidden\"\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"absolute left-0 top-0 rounded-full bg-accent @min-[390px]:rounded-[10px]\"\n                  style={{\n                    transform: `translate(${mark.x}px, ${mark.y}px)`,\n                    width: mark.w,\n                    height: mark.h,\n                    opacity: mark.on ? 1 : 0,\n                    transition: reduced\n                      ? \"none\"\n                      : `${mark.slide ? `transform 380ms ${EASE}, width 380ms ${EASE}, height 380ms ${EASE}, ` : \"\"}opacity 200ms ${EASE}`,\n                  }}\n                />\n                {presets.map((p, i) => (\n                  <button\n                    key={p.label}\n                    ref={(el) => {\n                      presetRefs.current[i] = el;\n                    }}\n                    type=\"button\"\n                    aria-pressed={i === presetIndex}\n                    onClick={() => choose(p)}\n                    className={`relative h-8 shrink-0 whitespace-nowrap rounded-full px-3 text-left text-[12.5px] transition-colors duration-200 @min-[390px]:rounded-[10px] @min-[390px]:px-2.5 ${FOCUS} ${\n                      i === presetIndex ? \"font-medium text-foreground\" : \"text-muted-foreground hover:text-foreground\"\n                    }`}\n                  >\n                    {p.label}\n                  </button>\n                ))}\n              </div>\n            )}\n\n            <div className=\"mx-auto\" style={{ width: \"calc(var(--cell) * 7)\" }}>\n              <div className=\"flex h-9 items-center justify-between\">\n                <button\n                  type=\"button\"\n                  aria-label=\"Previous month\"\n                  disabled={prevOff}\n                  onClick={() => step(-1)}\n                  className={`-ml-1 flex size-8 items-center justify-center rounded-full text-foreground transition-opacity hover:bg-accent disabled:pointer-events-none disabled:opacity-30 ${FOCUS}`}\n                >\n                  <ChevronLeft className=\"size-4\" aria-hidden=\"true\" />\n                </button>\n                <p className=\"text-[13px] font-medium text-foreground\" aria-live=\"polite\">\n                  <TextMorph>{monthTitle}</TextMorph>\n                </p>\n                <button\n                  type=\"button\"\n                  aria-label=\"Next month\"\n                  disabled={nextOff}\n                  onClick={() => step(1)}\n                  className={`-mr-1 flex size-8 items-center justify-center rounded-full text-foreground transition-opacity hover:bg-accent disabled:pointer-events-none disabled:opacity-30 ${FOCUS}`}\n                >\n                  <ChevronRight className=\"size-4\" aria-hidden=\"true\" />\n                </button>\n              </div>\n\n              <div className=\"flex text-[11px] text-muted-foreground\" aria-hidden=\"true\">\n                {weekdays.map((w, i) => (\n                  <span key={i} className=\"flex h-7 items-center justify-center\" style={{ width: \"var(--cell)\" }}>\n                    {w}\n                  </span>\n                ))}\n              </div>\n\n              <div ref={gridRef} role=\"grid\" aria-label={monthTitle} onKeyDown={onGridKey} onPointerLeave={() => setHover(null)} className=\"relative\">\n                {/* The band and its dots, laid out per month so they never glide across a month change. */}\n                <div key={monthKey} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n                  {weeks.map((row, r) => {\n                    let a = -1;\n                    let b = -1;\n                    if (band)\n                      row.forEach((d, c) => {\n                        if (d >= band.start && d <= band.end) {\n                          if (a === -1) a = c;\n                          b = c;\n                        }\n                      });\n                    const on = a !== -1 && band !== null;\n                    const opens = on && same(row[a], band.start);\n                    const closes = on && same(row[b], band.end);\n                    const after = band !== null && row[0] > band.end;\n                    return (\n                      <span\n                        key={r}\n                        className={`absolute ${anchor ? \"bg-primary/[0.07]\" : \"bg-primary/[0.1]\"}`}\n                        style={{\n                          top: `calc(var(--cell) * ${r} + ${INSET})`,\n                          height: \"var(--dot)\",\n                          left: on ? `calc(var(--cell) * ${a} + ${opens ? INSET : \"2px\"})` : after ? \"2px\" : \"calc(var(--cell) * 7 - 2px)\",\n                          width: on ? `calc(var(--cell) * ${b - a + 1} - ${opens ? INSET : \"2px\"} - ${closes ? INSET : \"2px\"})` : \"0px\",\n                          opacity: on ? 1 : 0,\n                          borderRadius: `${opens ? 999 : 8}px ${closes ? 999 : 8}px ${closes ? 999 : 8}px ${opens ? 999 : 8}px`,\n                          transition: reduced\n                            ? \"none\"\n                            : `left ${GLIDE}ms ${EASE}, width ${GLIDE}ms ${EASE}, opacity 220ms ${EASE}, border-radius 260ms ${EASE}, background-color 200ms`,\n                        }}\n                      />\n                    );\n                  })}\n                  {startAt && <Dot row={startAt.r} col={startAt.c} reduced={reduced} light={false} />}\n                  {endAt && <Dot row={endAt.r} col={endAt.c} reduced={reduced} light={Boolean(anchor && hover)} />}\n                </div>\n\n                {weeks.map((row, r) => (\n                  <div key={r} role=\"row\" className=\"relative flex\">\n                    {row.map((d) => {\n                      const inMonth = d.getMonth() === month.getMonth();\n                      const edge = band !== null && (same(d, band.start) || same(d, band.end));\n                      const off = blocked(d);\n                      return (\n                        <button\n                          key={d.toDateString()}\n                          ref={(el) => {\n                            if (el) dayRefs.current.set(d.toDateString(), el);\n                            else dayRefs.current.delete(d.toDateString());\n                          }}\n                          type=\"button\"\n                          role=\"gridcell\"\n                          tabIndex={same(d, tabDay) ? 0 : -1}\n                          aria-selected={band !== null && d >= band.start && d <= band.end}\n                          aria-disabled={off || undefined}\n                          aria-current={same(d, today) ? \"date\" : undefined}\n                          aria-label={longFmt.format(d)}\n                          onClick={() => {\n                            setFocusDay(d);\n                            pick(d);\n                          }}\n                          onPointerEnter={() => {\n                            if (anchor && !off) setHover(d);\n                          }}\n                          className={`group flex items-center justify-center outline-none ${off ? \"cursor-default\" : \"\"}`}\n                          style={{ width: \"var(--cell)\", height: \"var(--cell)\" }}\n                        >\n                          <span\n                            className={`flex items-center justify-center rounded-full text-[13px] tabular-nums group-focus-visible:ring-2 group-focus-visible:ring-ring/40 @min-[390px]:text-[12.5px] ${\n                              edge\n                                ? \"font-medium text-primary-foreground\"\n                                : off\n                                  ? \"text-muted-foreground/35\"\n                                  : `${inMonth ? \"text-foreground\" : \"text-muted-foreground/60\"} group-hover:bg-accent`\n                            } ${same(d, today) && !edge ? \"shadow-[inset_0_0_0_1px_var(--border)]\" : \"\"}`}\n                            style={{\n                              width: \"var(--dot)\",\n                              height: \"var(--dot)\",\n                              transition: reduced ? \"none\" : `color 220ms ${EASE} ${edge ? 90 : 0}ms, background-color 160ms`,\n                            }}\n                          >\n                            {d.getDate()}\n                          </span>\n                        </button>\n                      );\n                    })}\n                  </div>\n                ))}\n              </div>\n            </div>\n          </div>\n\n          {/* The range as it stands, morphing while you pick, and the way out. */}\n          <div className=\"flex items-center justify-between gap-3 border-t border-border py-2 pl-3.5 pr-2\">\n            <div className=\"min-w-0 leading-tight\">\n              <p className=\"truncate text-[12.5px] font-medium tabular-nums text-foreground\">\n                <TextMorph>{band ? label(band) : placeholder}</TextMorph>\n              </p>\n              <p className=\"mt-0.5 text-[11.5px] text-muted-foreground\" aria-hidden=\"true\">\n                <NumberRoll value={count} />{\" \"}\n                <TextMorph>{nights ? (count === 1 ? \"night\" : \"nights\") : count === 1 ? \"day\" : \"days\"}</TextMorph>\n              </p>\n            </div>\n            <div className=\"flex shrink-0 gap-1\">\n              <button\n                type=\"button\"\n                onClick={() => request(false)}\n                className={`h-8 rounded-full px-3 text-[12.5px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground ${FOCUS}`}\n              >\n                Cancel\n              </button>\n              <button\n                type=\"button\"\n                onClick={apply}\n                disabled={!ready}\n                className={`h-8 rounded-full bg-primary px-3.5 text-[12.5px] font-medium text-primary-foreground transition-opacity disabled:opacity-40 ${FOCUS}`}\n              >\n                Apply\n              </button>\n            </div>\n          </div>\n          <p className=\"sr-only\" role=\"status\">\n            {announce}\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n"}],"css":{},"docs":"Docs and live preview: https://hairlineui.com/ui/date-range-picker"}