{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "type": "registry:ui",
  "title": "Date range picker",
  "description": "A range picker that grows from its trigger into two months with presets and a stretching range highlight.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://mcp.xzero.co.uk/r/xzero-motion-tokens.json"
  ],
  "files": [
    {
      "path": "registry/components/date-range-picker/date-range-picker.tsx",
      "type": "registry:component",
      "target": "@components/xzero/date-range-picker/date-range-picker.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent, CSSProperties, FocusEvent as ReactFocusEvent, ReactNode } from \"react\";\nimport { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport type { Transition, Variants } from \"motion/react\";\nimport { CalendarDays, ChevronDown, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { useToday } from \"../lib/use-today\";\nimport styles from \"./date-range-picker.module.css\";\n\n/** An inclusive range of whole days. */\nexport interface DateRange {\n  start: Date;\n  end: Date;\n}\n\n/** A shortcut in the preset rail. `range` receives the viewer's local today. */\nexport interface DateRangePreset {\n  label: string;\n  range: (today: Date) => DateRange;\n}\n\n/**\n * A range picker whose trigger grows into the panel it opens. The panel shows two months beside a preset rail, or one\n * month with a scrolling preset row when space is tight. The range highlight stretches across each week as you hover\n * or move with the keyboard, the ends glide between days, months slide in the direction you travel, and on Apply the\n * formatted label flies back into the trigger as the surface shrinks around it.\n * Use it for reports, filters, and bookings where a start and end date are chosen together.\n */\nexport interface DateRangePickerProps {\n  /** Controlled value. Pass `null` for no selection. */\n  value?: DateRange | null;\n  /** Uncontrolled starting value. */\n  defaultValue?: DateRange | null;\n  /** Called with the applied range. */\n  onChange?: (range: DateRange) => void;\n  /** Accessible name of the trigger and the dialog. Defaults to \"Date range\". */\n  label?: string;\n  placeholder?: string;\n  presets?: DateRangePreset[];\n  minDate?: Date;\n  maxDate?: Date;\n  /** 0 is Sunday, 1 is Monday. Defaults to 0. */\n  weekStartsOn?: 0 | 1;\n  locale?: string;\n  /** Force one or two months. `auto` picks two when the boundary is wide enough. */\n  months?: \"auto\" | 1 | 2;\n  /** The element the panel should stay inside. Defaults to the viewport. */\n  boundary?: () => HTMLElement | null;\n  className?: string;\n}\n\ntype Size = { w: number; h: number };\ntype Bezier = [number, number, number, number];\n\nconst DAY = 864e5;\nconst startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());\nconst monthStart = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1);\nconst monthEnd = (date: Date) => new Date(date.getFullYear(), date.getMonth() + 1, 0);\nconst addDays = (date: Date, amount: number) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);\nconst addMonths = (date: Date, amount: number) => new Date(date.getFullYear(), date.getMonth() + amount, 1);\nconst shiftMonths = (date: Date, amount: number) => new Date(date.getFullYear(), date.getMonth() + amount, Math.min(date.getDate(), new Date(date.getFullYear(), date.getMonth() + amount + 1, 0).getDate()));\nconst dayDiff = (a: Date, b: Date) => Math.round((startOfDay(a).getTime() - startOfDay(b).getTime()) / DAY);\nconst monthDiff = (a: Date, b: Date) => (a.getFullYear() - b.getFullYear()) * 12 + a.getMonth() - b.getMonth();\nconst sameDay = (a?: Date | null, b?: Date | null) => Boolean(a && b && dayDiff(a, b) === 0);\nconst keyOf = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\nconst fromKey = (key: string) => { const [year, month, day] = key.split(\"-\").map(Number); return new Date(year!, month! - 1, day!); };\nconst ordered = (a: Date, b: Date): DateRange => dayDiff(a, b) <= 0 ? { start: a, end: b } : { start: b, end: a };\nconst sameRange = (a?: DateRange | null, b?: DateRange | null) => Boolean(a && b && sameDay(a.start, b.start) && sameDay(a.end, b.end));\nconst clampDate = (date: Date, min?: Date, max?: Date) => min && dayDiff(date, min) < 0 ? startOfDay(min) : max && dayDiff(date, max) > 0 ? startOfDay(max) : date;\n\nexport const defaultDateRangePresets: DateRangePreset[] = [\n  { label: \"Today\", range: today => ({ start: today, end: today }) },\n  { label: \"Yesterday\", range: today => ({ start: addDays(today, -1), end: addDays(today, -1) }) },\n  { label: \"Last 7 days\", range: today => ({ start: addDays(today, -6), end: today }) },\n  { label: \"Last 30 days\", range: today => ({ start: addDays(today, -29), end: today }) },\n  { label: \"This month\", range: today => ({ start: monthStart(today), end: today }) },\n  { label: \"Last month\", range: today => ({ start: addMonths(today, -1), end: monthEnd(addMonths(today, -1)) }) },\n  { label: \"This quarter\", range: today => ({ start: new Date(today.getFullYear(), Math.floor(today.getMonth() / 3) * 3, 1), end: today }) },\n  { label: \"Year to date\", range: today => ({ start: new Date(today.getFullYear(), 0, 1), end: today }) },\n];\n\n/** The viewer's local date (lib/use-today.ts, shared with calendar). Re-exported so existing imports from date-range-picker keep working. */\nexport { useToday } from \"../lib/use-today\";\n\nconst noop = () => () => {};\nfunction useReducedFlag() {\n  const hydrated = useSyncExternalStore(noop, () => true, () => false);\n  return !!useReducedMotion() && hydrated;\n}\n\nconst { blur } = motionTokens;\nconst enterEase = [...motionTokens.ease.enter] as Bezier;\nconst standardEase = [...motionTokens.ease.standard] as Bezier;\n/** Duration springs restated as stiffness and damping, so every retarget keeps the velocity already in flight. */\nconst physical = (visualDuration: number, bounce: number): Transition => {\n  const root = 2 * Math.PI / (visualDuration * 1.2);\n  return { type: \"spring\", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };\n};\nconst GROW = physical(.48, 0), SHRINK = physical(.38, 0), STRETCH = physical(.3, 0), GLIDE = physical(.3, 0), SLIDE = physical(.4, 0);\nconst TRIGGER_RADIUS = 18, PANEL_RADIUS = 26, WIDE_CELL = 36, WIDE_MIN = 712, EDGE = 8;\n\nconst roll: Variants = {\n  enter: (direction: number) => ({ opacity: 0, y: `${direction * .45}em`, filter: `blur(${blur.subtle}px)` }),\n  center: { opacity: 1, y: \"0em\", filter: \"blur(0px)\", transition: { duration: .26, ease: enterEase } },\n  exit: (direction: number) => ({ opacity: 0, y: `${direction * -.45}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .14, ease: standardEase } }),\n};\nconst fade: Variants = { enter: { opacity: 0 }, center: { opacity: 1, y: \"0em\", filter: \"blur(0px)\", transition: { duration: .12 } }, exit: { opacity: 0, transition: { duration: .08 } } };\n/** Panel content arrives just behind the growing surface and leaves ahead of it. */\nconst faceIn: Variants = {\n  hidden: { opacity: 0, y: -4, filter: `blur(${blur.soft}px)` },\n  shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: .26, ease: enterEase, delay: .07 } },\n  gone: { opacity: 0, y: -2, filter: `blur(${blur.soft}px)`, transition: { duration: .12, ease: standardEase } },\n};\nconst faceFade: Variants = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: .12 } }, gone: { opacity: 0, transition: { duration: .08 } } };\nconst slide: Variants = {\n  enter: (direction: number) => ({ opacity: 0, x: direction * 48 }),\n  center: { opacity: 1, x: 0, transition: { x: SLIDE, opacity: { duration: .22, ease: enterEase } } },\n  exit: (direction: number) => ({ opacity: 0, x: direction * -36, transition: { x: SLIDE, opacity: { duration: .14, ease: standardEase } } }),\n};\n\n/** Words roll one by one: a later date rises from below, an earlier one drops from above; words that stay the same hold still. */\nfunction Rolling({ text, direction, reduced }: { text: string; direction: number; reduced: boolean }) {\n  const words = text.split(/\\s+/).filter(Boolean);\n  return <span className={styles.rolling} aria-hidden=\"true\">\n    {words.map((word, index) => <span key={index} className={styles.word}>\n      <AnimatePresence initial={false} mode=\"popLayout\" custom={direction}>\n        <motion.span key={word} className={styles.wordInner} custom={direction} variants={reduced ? fade : roll} initial=\"enter\" animate=\"center\" exit=\"exit\">{word}</motion.span>\n      </AnimatePresence>\n    </span>)}\n  </span>;\n}\n\n/** Remembers which way a range moved, so labels roll in time's direction. */\nfunction useDirection(range: DateRange | null) {\n  const time = range ? range.start.getTime() * 2 + range.end.getTime() : null;\n  const [previous, setPrevious] = useState(time);\n  const [direction, setDirection] = useState(1);\n  if (time !== previous) { setPrevious(time); setDirection(time === null || previous === null || time >= previous ? 1 : -1); }\n  return direction;\n}\n\ntype Segment = { left: number; width: number } | null;\nconst cellIn = (date: Date, first: Date) => { const index = dayDiff(date, first); return { row: Math.floor(index / 7), col: index % 7 }; };\n\n/** One week's slice of the highlight. It grows out of the edge the range arrives from, so hovering reads as stretching. */\nfunction Bar({ row, segment, reduced }: { row: number; segment: Segment; reduced: boolean }) {\n  const left = useMotionValue(segment?.left ?? 0), width = useMotionValue(segment?.width ?? 0), opacity = useMotionValue(segment ? 1 : 0);\n  const leftPct = useTransform(left, value => `${value}%`), widthPct = useTransform(width, value => `${value}%`);\n  const wasEmpty = useRef(!segment);\n  useLayoutEffect(() => {\n    if (!segment) { wasEmpty.current = true; animate(opacity, 0, { duration: reduced ? 0 : .14 }); return; }\n    if (wasEmpty.current || reduced) { left.jump(segment.left); width.jump(segment.width); }\n    else { animate(left, segment.left, STRETCH); animate(width, segment.width, STRETCH); }\n    wasEmpty.current = false;\n    animate(opacity, 1, { duration: reduced ? 0 : .16 });\n  }, [left, opacity, reduced, segment, width]);\n  return <motion.span className={styles.bar} style={{ left: leftPct, width: widthPct, opacity, top: `calc(${row} * var(--cell) + 3px)` }} />;\n}\n\n/** A solid end of the range. It glides from day to day and only fades when the end leaves this month. */\nfunction Thumb({ cell, reduced }: { cell: { row: number; col: number } | null; reduced: boolean }) {\n  const x = useMotionValue(cell?.col ?? 0), y = useMotionValue(cell?.row ?? 0), opacity = useMotionValue(cell ? 1 : 0), scale = useMotionValue(cell ? 1 : .8);\n  const transform = useTransform(() => `translate(${x.get() * 100}%, ${y.get() * 100}%) scale(${scale.get()})`);\n  const hidden = useRef(!cell);\n  useLayoutEffect(() => {\n    if (!cell) { hidden.current = true; animate(opacity, 0, { duration: reduced ? 0 : .12 }); animate(scale, .8, { duration: reduced ? 0 : .12 }); return; }\n    if (hidden.current || reduced) { x.jump(cell.col); y.jump(cell.row); }\n    else { animate(x, cell.col, GLIDE); animate(y, cell.row, GLIDE); }\n    hidden.current = false;\n    animate(opacity, 1, { duration: reduced ? 0 : .16 });\n    animate(scale, 1, reduced ? { duration: 0 } : GLIDE);\n  }, [cell, opacity, reduced, scale, x, y]);\n  return <motion.span className={styles.thumb} style={{ transform, opacity }} />;\n}\n\ninterface MonthProps {\n  month: Date;\n  range: DateRange | null;\n  tabbable: string;\n  today?: Date;\n  minDate?: Date;\n  maxDate?: Date;\n  weekStartsOn: 0 | 1;\n  reduced: boolean;\n  formatters: { title: Intl.DateTimeFormat; day: Intl.DateTimeFormat; weekday: Intl.DateTimeFormat; weekdayLong: Intl.DateTimeFormat };\n  onPick: (date: Date) => void;\n  onHover: (date: Date) => void;\n  onKey: (event: ReactKeyboardEvent<HTMLButtonElement>, date: Date) => void;\n  onFocusDay: (date: Date) => void;\n  idBase: string;\n}\n\nfunction Month({ month, range, tabbable, today, minDate, maxDate, weekStartsOn, reduced, formatters, onPick, onHover, onKey, onFocusDay, idBase }: MonthProps) {\n  const first = useMemo(() => addDays(month, -((month.getDay() - weekStartsOn + 7) % 7)), [month, weekStartsOn]);\n  const rows = useMemo(() => Array.from({ length: 6 }, (_, row) => Array.from({ length: 7 }, (_, col) => addDays(first, row * 7 + col))), [first]);\n  const titleId = `${idBase}-${keyOf(month)}`;\n  const inMonth = (date: Date) => monthDiff(date, month) === 0;\n\n  const lo = range?.start, hi = range?.end;\n  const loKey = lo ? keyOf(lo) : \"\", hiKey = hi ? keyOf(hi) : \"\";\n  /** Each week's slice of the range, in percent of the row. Keyed by the range's days so bars only move when it does. */\n  const segments = useMemo<Segment[]>(() => {\n    if (!loKey || !hiKey) return rows.map(() => null);\n    const start = fromKey(loKey), end = fromKey(hiKey);\n    const unit = 100 / 7;\n    const colOf = (date: Date) => dayDiff(date, first) % 7;\n    return rows.map(row => {\n      const days = row.filter(date => monthDiff(date, month) === 0);\n      if (!days.length) return null;\n      const rowFirst = days[0]!, rowLast = days[days.length - 1]!;\n      const from = dayDiff(start, rowFirst) > 0 ? start : rowFirst, to = dayDiff(end, rowLast) < 0 ? end : rowLast;\n      if (dayDiff(from, to) <= 0) { const a = colOf(from), b = colOf(to); return { left: a * unit, width: (b - a + 1) * unit }; }\n      // The week sits wholly before or after the range: collapse at the edge the range would grow in from.\n      if (dayDiff(rowLast, start) < 0) return { left: (colOf(rowLast) + 1) * unit, width: 0 };\n      return { left: colOf(rowFirst) * unit, width: 0 };\n    });\n  }, [first, hiKey, loKey, month, rows]);\n  const startCell = useMemo(() => loKey && monthDiff(fromKey(loKey), month) === 0 ? cellIn(fromKey(loKey), first) : null, [loKey, month, first]);\n  const endCell = useMemo(() => hiKey && hiKey !== loKey && monthDiff(fromKey(hiKey), month) === 0 ? cellIn(fromKey(hiKey), first) : null, [hiKey, loKey, month, first]);\n\n  return <div className={styles.month}>\n    <p id={titleId} className={styles.monthTitle}>{formatters.title.format(month)}</p>\n    <div role=\"grid\" aria-labelledby={titleId} className={styles.grid}>\n      <div role=\"row\" className={styles.weekdays}>\n        {rows[0]!.map(date => <span key={date.getDay()} role=\"columnheader\" aria-label={formatters.weekdayLong.format(date)}>{formatters.weekday.format(date).slice(0, 2)}</span>)}\n      </div>\n      <div className={styles.weeks}>\n        <span className={styles.layer} aria-hidden=\"true\">\n          {segments.map((segment, row) => <Bar key={row} row={row} segment={segment} reduced={reduced} />)}\n          <Thumb cell={startCell} reduced={reduced} />\n          <Thumb cell={endCell} reduced={reduced} />\n        </span>\n        {rows.map((row, index) => <div key={index} role=\"row\" className={styles.week}>\n          {row.map(date => {\n            if (!inMonth(date)) return <span key={keyOf(date)} role=\"gridcell\" className={styles.blank} />;\n            const key = keyOf(date);\n            const disabled = Boolean((minDate && dayDiff(date, minDate) < 0) || (maxDate && dayDiff(date, maxDate) > 0));\n            const inRange = Boolean(lo && hi && dayDiff(date, lo) >= 0 && dayDiff(date, hi) <= 0);\n            const edge = sameDay(date, lo) || sameDay(date, hi);\n            const isToday = sameDay(date, today);\n            return <span key={key} role=\"gridcell\" aria-selected={inRange} className={styles.cell}>\n              <button type=\"button\" className={styles.day} data-date={key} data-edge={edge || undefined} data-range={inRange || undefined} data-today={isToday || undefined}\n                tabIndex={key === tabbable ? 0 : -1} disabled={disabled} aria-current={isToday ? \"date\" : undefined}\n                aria-label={formatters.day.format(date)}\n                onClick={() => onPick(date)} onPointerEnter={() => { if (!disabled) onHover(date); }}\n                onFocus={() => onFocusDay(date)} onKeyDown={event => onKey(event, date)}>\n                <span>{date.getDate()}</span>\n              </button>\n            </span>;\n          })}\n        </div>)}\n      </div>\n    </div>\n  </div>;\n}\n\n/** The visible months travel together; a leaving set turns inert so focus and queries only find the current one. */\nfunction Months({ children, direction, reduced }: { children: ReactNode; direction: number; reduced: boolean }) {\n  const present = useIsPresent();\n  return <motion.div className={styles.months} data-current={present || undefined} inert={!present || undefined} custom={direction}\n    variants={reduced ? fade : slide} initial=\"enter\" animate=\"center\" exit=\"exit\">{children}</motion.div>;\n}\n\nexport function DateRangePicker({ value, defaultValue = null, onChange, label = \"Date range\", placeholder = \"Select dates\", presets = defaultDateRangePresets, minDate, maxDate, weekStartsOn = 0, locale = \"en-US\", months = \"auto\", boundary, className }: DateRangePickerProps) {\n  const reduced = useReducedFlag();\n  const today = useToday();\n  const uid = useId();\n  const [inner, setInner] = useState<DateRange | null>(defaultValue);\n  const committed = value !== undefined ? value : inner;\n\n  const [open, setOpen] = useState(false);\n  const [compact, setCompact] = useState(false);\n  const [panelWidth, setPanelWidth] = useState(0);\n  const [view, setView] = useState<Date>(() => monthStart(committed?.end ?? new Date(2000, 0, 1)));\n  const [direction, setDirection] = useState(1);\n  const [draft, setDraft] = useState<DateRange | null>(committed);\n  const [anchor, setAnchor] = useState<Date | null>(null);\n  const [hover, setHover] = useState<Date | null>(null);\n  const [focusKey, setFocusKey] = useState(\"\");\n\n  const formatters = useMemo(() => ({\n    label: new Intl.DateTimeFormat(locale, { month: \"short\", day: \"numeric\", year: \"numeric\" }),\n    title: new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }),\n    day: new Intl.DateTimeFormat(locale, { weekday: \"long\", month: \"long\", day: \"numeric\", year: \"numeric\" }),\n    weekday: new Intl.DateTimeFormat(locale, { weekday: \"short\" }),\n    weekdayLong: new Intl.DateTimeFormat(locale, { weekday: \"long\" }),\n  }), [locale]);\n  const format = useCallback((range: DateRange | null) => !range ? placeholder : sameDay(range.start, range.end) ? formatters.label.format(range.start) : formatters.label.formatRange(range.start, range.end), [formatters, placeholder]);\n\n  const count = compact ? 1 : 2;\n  const visible = useMemo(() => Array.from({ length: count }, (_, index) => addMonths(view, index)), [count, view]);\n  const shown = anchor ? ordered(anchor, hover ?? anchor) : draft;\n  const activePreset = today && !anchor ? presets.findIndex(preset => sameRange(preset.range(today), draft)) : -1;\n  const days = shown ? dayDiff(shown.end, shown.start) + 1 : 0;\n  const committedDirection = useDirection(committed);\n  const shownDirection = useDirection(shown);\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const railRef = useRef<HTMLDivElement>(null);\n  const pendingFocus = useRef<\"trigger\" | \"day\" | null>(null);\n\n  /* ---------- One surface: the trigger's box springs to the panel's box and back ---------- */\n  // Before the first measurement the surface simply fills the root and the root wraps the trigger, so server markup already looks right.\n  const width = useMotionValue<number | string>(\"100%\"), height = useMotionValue<number | string>(\"100%\"), radius = useMotionValue(TRIGGER_RADIUS), x = useMotionValue(0), rootWidth = useMotionValue<number | string>(\"auto\");\n  const sizes = useRef<{ trigger: Size; panel: Size | null }>({ trigger: { w: 0, h: 0 }, panel: null });\n  const live = useRef({ open: false, reduced: false, ready: false });\n  const queued = useRef(false);\n  const boundaryRef = useRef(boundary);\n  useLayoutEffect(() => { boundaryRef.current = boundary; }, [boundary]);\n\n  const bounds = useCallback(() => {\n    const element = boundaryRef.current?.();\n    const viewport = { left: 0, right: document.documentElement.clientWidth };\n    if (!element) return viewport;\n    const rect = element.getBoundingClientRect();\n    return { left: Math.max(viewport.left, rect.left), right: Math.min(viewport.right, rect.right) };\n  }, []);\n\n  const update = useCallback(() => {\n    queued.current = false;\n    const { trigger, panel } = sizes.current, state = live.current;\n    if (!trigger.w) return;\n    const openPanel = state.open && panel ? panel : null;\n    const target = openPanel ?? trigger;\n    let offset = 0;\n    if (openPanel && rootRef.current) {\n      const rect = rootRef.current.getBoundingClientRect(), box = bounds();\n      offset = Math.min(0, box.right - EDGE - (rect.left + openPanel.w));\n      offset = Math.max(offset, box.left + EDGE - rect.left);\n    }\n    const r = openPanel ? PANEL_RADIUS : TRIGGER_RADIUS;\n    if (!state.ready || state.reduced) {\n      width.jump(target.w); height.jump(target.h); radius.jump(r); x.jump(offset); rootWidth.jump(trigger.w);\n      state.ready = true;\n      return;\n    }\n    const spring = openPanel ? GROW : state.open ? GROW : SHRINK;\n    animate(width, target.w, spring); animate(height, target.h, spring); animate(radius, r, spring); animate(x, offset, spring);\n    animate(rootWidth, trigger.w, motionTokens.spring.morph);\n  }, [bounds, height, radius, rootWidth, width, x]);\n\n  const schedule = useCallback(() => {\n    if (queued.current) return;\n    queued.current = true;\n    queueMicrotask(update);\n  }, [update]);\n\n  useLayoutEffect(() => { live.current.open = open; live.current.reduced = reduced; if (!open) sizes.current.panel = null; schedule(); }, [open, reduced, schedule]);\n\n  useLayoutEffect(() => {\n    const node = triggerRef.current;\n    if (!node) return;\n    const read = () => { sizes.current.trigger = { w: node.offsetWidth, h: node.offsetHeight }; schedule(); };\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [schedule]);\n\n  useLayoutEffect(() => {\n    const node = panelRef.current;\n    if (!open || !node) return;\n    const read = () => { sizes.current.panel = { w: node.offsetWidth, h: node.offsetHeight }; schedule(); };\n    read();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [open, schedule]);\n\n  // On a short screen (a phone, a trigger low on the page) the panel can open below the fold, with Apply out of reach.\n  // Once it has its size, scroll just enough to bring its bottom into view, never past its top. After closing, the\n  // trigger comes back into view if that scroll left it off screen.\n  useEffect(() => {\n    if (!open) return;\n    const frame = requestAnimationFrame(() => {\n      const node = panelRef.current;\n      if (!node) return;\n      const rect = node.getBoundingClientRect();\n      const overflow = rect.bottom - (window.innerHeight - EDGE);\n      if (overflow <= 0) return;\n      window.scrollBy({ top: Math.min(overflow, Math.max(0, rect.top - EDGE)), behavior: reduced ? \"instant\" : \"smooth\" });\n    });\n    return () => cancelAnimationFrame(frame);\n  }, [open, reduced]);\n  useEffect(() => {\n    if (open) return;\n    const trigger = triggerRef.current;\n    if (!trigger) return;\n    const rect = trigger.getBoundingClientRect();\n    if (rect.bottom < 0 || rect.top > window.innerHeight) trigger.scrollIntoView({ block: \"center\", behavior: reduced ? \"instant\" : \"smooth\" });\n  }, [open, reduced]);\n\n  /* ---------- Layout: two months when there is room, one compact month otherwise ---------- */\n  const measureLayout = useCallback(() => {\n    const box = bounds();\n    const available = Math.max(0, box.right - box.left - EDGE * 2);\n    const single = months === 1 || (months === \"auto\" && available < WIDE_MIN);\n    return { single, width: Math.min(352, available) };\n  }, [bounds, months]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onResize = () => { const next = measureLayout(); setCompact(next.single); setPanelWidth(next.width); schedule(); };\n    window.addEventListener(\"resize\", onResize);\n    return () => window.removeEventListener(\"resize\", onResize);\n  }, [measureLayout, open, schedule]);\n\n  const viewFor = (range: DateRange | null, single: boolean, fallback: Date) => {\n    const end = monthStart(range?.end ?? fallback);\n    return single ? end : addMonths(end, -1);\n  };\n\n  const openPanel = () => {\n    if (!today) return;\n    const layout = measureLayout();\n    setCompact(layout.single);\n    setPanelWidth(layout.width);\n    setDraft(committed);\n    setAnchor(null);\n    setHover(null);\n    setView(viewFor(committed, layout.single, today));\n    setDirection(0);\n    setFocusKey(keyOf(committed?.start ?? today));\n    pendingFocus.current = \"day\";\n    setOpen(true);\n  };\n\n  const close = useCallback((focus: boolean) => {\n    if (focus) pendingFocus.current = \"trigger\";\n    setDirection(0);\n    setOpen(false);\n    setAnchor(null);\n    setHover(null);\n  }, []);\n\n  const apply = () => {\n    const next = anchor ? { start: anchor, end: anchor } : draft;\n    if (!next) return;\n    if (value === undefined) setInner(next);\n    onChange?.(next);\n    close(true);\n  };\n\n  /* ---------- Focus management ---------- */\n  useLayoutEffect(() => {\n    const target = pendingFocus.current;\n    if (!target) return;\n    if (target === \"trigger\" && !open) { pendingFocus.current = null; triggerRef.current?.focus({ preventScroll: true }); return; }\n    if (target === \"day\" && open) {\n      const node = panelRef.current?.querySelector<HTMLButtonElement>(`[data-current] [data-date=\"${focusKey}\"]`);\n      if (node) { pendingFocus.current = null; node.focus({ preventScroll: true }); }\n    }\n  }, [focusKey, open, view, compact]);\n\n  useEffect(() => {\n    if (!open) return;\n    const down = (event: PointerEvent) => { if (!rootRef.current?.contains(event.target as Node)) close(false); };\n    document.addEventListener(\"pointerdown\", down);\n    return () => document.removeEventListener(\"pointerdown\", down);\n  }, [close, open]);\n\n  const onRootKey = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\" && open) { event.preventDefault(); event.stopPropagation(); close(true); }\n  };\n  const onRootBlur = (event: ReactFocusEvent<HTMLDivElement>) => {\n    const next = event.relatedTarget as Node | null;\n    if (open && next && !event.currentTarget.contains(next)) close(false);\n  };\n\n  /* ---------- Month navigation and picking ---------- */\n  const goTo = (next: Date) => {\n    const delta = monthDiff(next, view);\n    if (!delta) return;\n    setDirection(Math.sign(delta));\n    setView(next);\n  };\n\n  /** Keeps a range on screen: nothing moves when it already fits, otherwise its last month lands on the right. */\n  const reveal = (range: DateRange) => {\n    const first = view, last = addMonths(view, count - 1);\n    if (monthDiff(range.start, first) >= 0 && monthDiff(range.end, last) <= 0) return;\n    goTo(viewFor(range, compact, range.end));\n  };\n\n  const pick = (date: Date) => {\n    setFocusKey(keyOf(date));\n    if (!anchor) { setAnchor(date); setHover(date); return; }\n    setDraft(ordered(anchor, date));\n    setAnchor(null);\n    setHover(null);\n  };\n\n  const choosePreset = (preset: DateRangePreset) => {\n    if (!today) return;\n    const range = preset.range(today);\n    setDraft(range);\n    setAnchor(null);\n    setHover(null);\n    setFocusKey(keyOf(range.start));\n    reveal(range);\n  };\n\n  const onDayKey = (event: ReactKeyboardEvent<HTMLButtonElement>, date: Date) => {\n    const weekday = (date.getDay() - weekStartsOn + 7) % 7;\n    const moves: Record<string, () => Date> = {\n      ArrowLeft: () => addDays(date, -1), ArrowRight: () => addDays(date, 1), ArrowUp: () => addDays(date, -7), ArrowDown: () => addDays(date, 7),\n      Home: () => addDays(date, -weekday), End: () => addDays(date, 6 - weekday),\n      PageUp: () => shiftMonths(date, event.shiftKey ? -12 : -1), PageDown: () => shiftMonths(date, event.shiftKey ? 12 : 1),\n    };\n    const move = moves[event.key];\n    if (!move) return;\n    event.preventDefault();\n    const next = clampDate(move(), minDate, maxDate);\n    setFocusKey(keyOf(next));\n    if (anchor) setHover(next);\n    pendingFocus.current = \"day\";\n    const first = view, last = addMonths(view, count - 1);\n    if (monthDiff(next, first) < 0) goTo(monthStart(next));\n    else if (monthDiff(next, last) > 0) goTo(addMonths(monthStart(next), -(count - 1)));\n  };\n\n  const onRailKey = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n    const keys = compact ? [\"ArrowLeft\", \"ArrowRight\"] : [\"ArrowUp\", \"ArrowDown\"];\n    if (!keys.includes(event.key)) return;\n    const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"[data-preset]\"));\n    const at = buttons.indexOf(document.activeElement as HTMLButtonElement);\n    if (at < 0) return;\n    event.preventDefault();\n    buttons[(at + (event.key === keys[1] ? 1 : -1) + buttons.length) % buttons.length]?.focus();\n  };\n\n  /** The day that owns the tab stop: the focused day when it is on screen, else the first visible day of the range, else the 1st. */\n  const tabbable = useMemo(() => {\n    const onScreen = (date: Date) => visible.some(month => monthDiff(date, month) === 0);\n    if (focusKey && onScreen(fromKey(focusKey))) return focusKey;\n    if (shown && onScreen(shown.start)) return keyOf(shown.start);\n    return keyOf(visible[0]!);\n  }, [focusKey, shown, visible]);\n\n  /* ---------- Preset highlight glides between presets ---------- */\n  const gx = useMotionValue(0), gy = useMotionValue(0), gw = useMotionValue(0), gh = useMotionValue(0), go = useMotionValue(0);\n  useLayoutEffect(() => {\n    const rail = railRef.current;\n    const node = activePreset < 0 ? null : rail?.querySelector<HTMLElement>(`[data-preset=\"${activePreset}\"]`);\n    if (!rail || !node) { animate(go, 0, { duration: reduced ? 0 : .14 }); return; }\n    const place = [node.offsetLeft, node.offsetTop, node.offsetWidth, node.offsetHeight] as const;\n    if (go.get() < .05 || reduced) { gx.jump(place[0]); gy.jump(place[1]); gw.jump(place[2]); gh.jump(place[3]); }\n    else { animate(gx, place[0], GLIDE); animate(gy, place[1], GLIDE); animate(gw, place[2], GLIDE); animate(gh, place[3], GLIDE); }\n    animate(go, 1, { duration: reduced ? 0 : .16 });\n    if (compact && (node.offsetLeft < rail.scrollLeft || node.offsetLeft + node.offsetWidth > rail.scrollLeft + rail.clientWidth)) {\n      rail.scrollTo({ left: node.offsetLeft - 12, behavior: reduced ? \"auto\" : \"smooth\" });\n    }\n  }, [activePreset, compact, go, gh, gw, gx, gy, open, reduced]);\n\n  /* ---------- Copy ---------- */\n  const committedText = format(committed);\n  const shownText = shown ? format(shown) : \"No dates\";\n  const countText = shown ? `${days} ${days === 1 ? \"day\" : \"days\"}` : \"\";\n  const status = !open ? \"\" : anchor ? `Start ${formatters.label.format(anchor)}. Choose an end date.` : shown ? `${shownText}, ${countText}` : \"\";\n  const layoutId = reduced ? undefined : `${uid}-value`;\n\n  const cellSize = compact && panelWidth ? Math.max(32, Math.min(42, Math.floor((panelWidth - 24) / 7))) : WIDE_CELL;\n\n  const quiet = open ? (reduced ? { opacity: 0 } : { opacity: 0, filter: `blur(${blur.subtle}px)` }) : { opacity: 1, filter: \"blur(0px)\" };\n  const quietTransition = open ? { duration: .12, ease: standardEase } : { duration: .22, ease: enterEase, delay: reduced ? 0 : .1 };\n\n  return <motion.div ref={rootRef} className={[styles.root, className].filter(Boolean).join(\" \")} style={{ width: rootWidth }} data-open={open || undefined} onKeyDown={onRootKey} onBlur={onRootBlur}>\n    {/* The trigger stays in place under the panel. Its icons fade, while the value itself flies to the panel footer and back. */}\n    <button ref={triggerRef} type=\"button\" className={styles.trigger} aria-haspopup=\"dialog\" aria-expanded={open} aria-controls={open ? `${uid}-panel` : undefined}\n      aria-label={`${label}: ${committedText}`} disabled={!today} inert={open || undefined} onClick={openPanel}>\n      <motion.span className={styles.triggerIcon} initial={false} animate={quiet} transition={quietTransition}><CalendarDays size={16} strokeWidth={1.75} aria-hidden=\"true\" /></motion.span>\n      {open || !layoutId\n        ? <motion.span key=\"resting\" className={styles.value} data-empty={!committed || undefined} initial={false} animate={open ? { opacity: 0 } : { opacity: 1 }} transition={quietTransition}>\n          <Rolling text={committedText} direction={committedDirection} reduced={reduced} />\n        </motion.span>\n        : <motion.span key=\"shared\" layoutId={layoutId} layout=\"position\" className={styles.value} data-empty={!committed || undefined} transition={SHRINK}>\n          <Rolling text={committedText} direction={committedDirection} reduced={reduced} />\n        </motion.span>}\n      <motion.span className={styles.chevron} initial={false} animate={quiet} transition={quietTransition}><ChevronDown size={16} strokeWidth={1.75} aria-hidden=\"true\" /></motion.span>\n    </button>\n\n    <motion.div className={styles.surface} style={{ width, height, borderRadius: radius, x }}>\n      <AnimatePresence>\n        {open && today && <motion.div key=\"panel\" ref={panelRef} id={`${uid}-panel`} className={styles.panel} role=\"dialog\" aria-label={label} data-compact={compact || undefined}\n          style={{ \"--cell\": `${cellSize}px`, width: compact && panelWidth ? panelWidth : undefined } as CSSProperties} exit={{ opacity: 1, transition: { duration: .14 } }}>\n          <motion.div className={styles.body} variants={reduced ? faceFade : faceIn} initial=\"hidden\" animate=\"shown\" exit=\"gone\">\n            <div ref={railRef} className={styles.rail} role=\"group\" aria-label=\"Presets\" onKeyDown={onRailKey}>\n              <motion.span className={styles.railHighlight} style={{ x: gx, y: gy, width: gw, height: gh, opacity: go }} aria-hidden=\"true\" />\n              {presets.map((preset, index) => <button key={preset.label} type=\"button\" className={styles.preset} data-preset={index} aria-pressed={index === activePreset}\n                onClick={() => choosePreset(preset)}>{preset.label}</button>)}\n            </div>\n\n            <div className={styles.calendars} onPointerLeave={() => { if (anchor) setHover(null); }}>\n              <button type=\"button\" className={styles.navButton} data-side=\"prev\" aria-label=\"Previous month\" onClick={() => goTo(addMonths(view, -1))}\n                disabled={Boolean(minDate && monthDiff(view, minDate) <= 0)}><ChevronLeft size={16} strokeWidth={1.75} aria-hidden=\"true\" /></button>\n              <button type=\"button\" className={styles.navButton} data-side=\"next\" aria-label=\"Next month\" onClick={() => goTo(addMonths(view, 1))}\n                disabled={Boolean(maxDate && monthDiff(addMonths(view, count - 1), maxDate) >= 0)}><ChevronRight size={16} strokeWidth={1.75} aria-hidden=\"true\" /></button>\n              {/* The viewport keeps a fixed six-week height and stacks the leaving and arriving months in one cell, so the panel never resizes mid-slide. */}\n              <div className={styles.viewport}>\n                <AnimatePresence initial={false} custom={direction}>\n                  <Months key={`${keyOf(view)}-${count}`} direction={direction} reduced={reduced}>\n                    {visible.map(month => <Month key={keyOf(month)} month={month} range={shown} tabbable={tabbable} today={today} minDate={minDate} maxDate={maxDate}\n                      weekStartsOn={weekStartsOn} reduced={reduced} formatters={formatters} idBase={uid}\n                      onPick={pick} onHover={date => { if (anchor) setHover(date); }} onKey={onDayKey} onFocusDay={date => setFocusKey(keyOf(date))} />)}\n                  </Months>\n                </AnimatePresence>\n              </div>\n            </div>\n          </motion.div>\n\n          <div className={styles.footer}>\n            <div className={styles.summary}>\n              <motion.span layoutId={layoutId} layout={layoutId ? \"position\" : undefined} className={styles.summaryValue} transition={GROW}\n                variants={layoutId ? undefined : faceFade} initial={layoutId ? undefined : \"hidden\"} animate={layoutId ? undefined : \"shown\"} exit={layoutId ? undefined : \"gone\"}>\n                <Rolling text={shownText} direction={shownDirection} reduced={reduced} />\n              </motion.span>\n              <motion.span className={styles.summaryCount} variants={reduced ? faceFade : faceIn} initial=\"hidden\" animate=\"shown\" exit=\"gone\">\n                {countText && <Rolling text={anchor && sameDay(anchor, hover) ? \"Pick an end date\" : countText} direction={shownDirection} reduced={reduced} />}\n              </motion.span>\n            </div>\n            <motion.div className={styles.actions} variants={reduced ? faceFade : faceIn} initial=\"hidden\" animate=\"shown\" exit=\"gone\">\n              <button type=\"button\" className={styles.ghost} onClick={() => close(true)}>Cancel</button>\n              <button type=\"button\" className={styles.primary} onClick={apply} disabled={!shown}>Apply</button>\n            </motion.div>\n          </div>\n          <p className={styles.srOnly} aria-live=\"polite\">{status}</p>\n        </motion.div>}\n      </AnimatePresence>\n    </motion.div>\n  </motion.div>;\n}\n\nexport default DateRangePicker;\n"
    },
    {
      "path": "lib/use-today.ts",
      "type": "registry:lib",
      "target": "@components/xzero/lib/use-today.ts",
      "content": "import { useMemo, useSyncExternalStore } from \"react\";\n\n/** Shared by calendar and date-range-picker, which both re-export it, so one copy keeps their \"today\" in step. */\nconst dateKey = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\nconst fromKey = (key: string) => {\n  const [year = 1970, month = 1, day = 1] = key.split(\"-\").map(Number);\n  return new Date(year, month - 1, day);\n};\n\n/** Today turns over at local midnight; returning to the tab, focusing the window or waking the device reads it again. */\nconst subscribeToday = (notify: () => void) => {\n  let timer = 0;\n  const schedule = () => {\n    const now = new Date();\n    const nextMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);\n    timer = window.setTimeout(() => { notify(); schedule(); }, nextMidnight.getTime() - now.getTime() + 1000);\n  };\n  const onVisible = () => { if (document.visibilityState === \"visible\") notify(); };\n  schedule();\n  document.addEventListener(\"visibilitychange\", onVisible);\n  window.addEventListener(\"focus\", notify);\n  return () => { window.clearTimeout(timer); document.removeEventListener(\"visibilitychange\", onVisible); window.removeEventListener(\"focus\", notify); };\n};\nconst readToday = () => dateKey(new Date());\nconst serverToday = () => \"\";\n\n/** The viewer's local date. Undefined on the server and during hydration, so markup never depends on the server clock or time zone; afterwards it follows the real date across midnight. */\nexport function useToday() {\n  const key = useSyncExternalStore(subscribeToday, readToday, serverToday);\n  return useMemo(() => (key ? fromKey(key) : undefined), [key]);\n}\n"
    },
    {
      "path": "registry/components/date-range-picker/date-range-picker.module.css",
      "type": "registry:component",
      "target": "@components/xzero/date-range-picker/date-range-picker.module.css",
      "content": "/* The root holds the trigger's place in the page. The surface floats above it and grows into the panel, so opening never pushes content. */\n.root {\n  position: relative;\n  display: inline-block;\n  height: var(--control-height-md);\n  vertical-align: top;\n  color: var(--foreground);\n  font-family: var(--font-body);\n  font-size: var(--text-sm);\n  letter-spacing: var(--tracking-body);\n  line-height: var(--leading-body);\n}\n.root { z-index: 0; transition: z-index 0s linear .5s; }\n.root[data-open] { z-index: 30; transition-delay: 0s; }\n.root:not([data-open]) .surface { z-index: 0; }\n.root *, .root *::before, .root *::after { box-sizing: border-box; }\n\n/* One material from trigger to panel. Size, corners, and offset come from springs; the border is an overlay so it never changes a measurement. */\n.surface {\n  position: absolute;\n  z-index: 2;\n  top: 0;\n  left: 0;\n  overflow: clip;\n  background: var(--surface-raised);\n  box-shadow: var(--shadow-resting);\n  transition: box-shadow var(--duration-considered) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard);\n}\n.surface::after { content: \"\"; position: absolute; inset: 0; border: 1px solid var(--border); border-radius: inherit; pointer-events: none; transition: border-color var(--duration-fast) var(--ease-standard); }\n.root[data-open] .surface { box-shadow: var(--shadow-floating); }\n\n/* Trigger */\n/* The trigger sits in flow above the surface while closed; when open, the surface and its panel rise over it. */\n.trigger {\n  position: relative;\n  z-index: 1;\n  display: inline-flex;\n  width: max-content;\n  height: var(--control-height-md);\n  align-items: center;\n  gap: 10px;\n  padding: 0 12px 0 14px;\n  border: 0;\n  background: transparent;\n  color: var(--foreground);\n  cursor: pointer;\n  font: inherit;\n  font-weight: 500;\n  white-space: nowrap;\n  -webkit-tap-highlight-color: transparent;\n}\n.trigger:disabled { cursor: default; }\n.trigger[inert] { pointer-events: none; }\n.triggerIcon, .chevron { display: grid; flex: none; place-items: center; color: var(--text-secondary); }\n.chevron { color: var(--text-muted); }\n.value { display: inline-flex; font-variant-numeric: tabular-nums; }\n.value[data-empty] { color: var(--text-secondary); font-weight: 400; }\n\n/* Words roll in their own slots; a leaving word pops out of flow so the next one never waits for space. */\n.rolling { display: inline-flex; gap: .28em; white-space: nowrap; }\n.word { position: relative; display: inline-block; }\n.wordInner { display: inline-block; }\n\n/* Panel */\n.panel { position: absolute; top: 0; left: 0; width: max-content; padding: 10px; --cell: 36px; }\n.body { display: flex; gap: 10px; }\n\n.rail { position: relative; display: flex; width: 9.25rem; flex: none; flex-direction: column; gap: 2px; padding-right: 10px; border-right: 1px solid var(--border-subtle); }\n.railHighlight { position: absolute; top: 0; left: 0; border-radius: 12px; background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }\n.preset {\n  position: relative;\n  display: flex;\n  height: 32px;\n  flex: none;\n  align-items: center;\n  padding: 0 12px;\n  border: 0;\n  border-radius: 12px;\n  background: transparent;\n  color: var(--text-secondary);\n  cursor: pointer;\n  font: inherit;\n  text-align: left;\n  white-space: nowrap;\n  -webkit-tap-highlight-color: transparent;\n  transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard);\n}\n.preset[aria-pressed=\"true\"] { color: var(--foreground); font-weight: 500; }\n\n.calendars { position: relative; padding: 0 2px; }\n.navButton {\n  position: absolute;\n  z-index: 2;\n  top: 0;\n  display: grid;\n  width: 32px;\n  height: 32px;\n  place-items: center;\n  padding: 0;\n  border: 0;\n  border-radius: var(--radius-pill);\n  background: transparent;\n  color: var(--text-secondary);\n  cursor: pointer;\n  -webkit-tap-highlight-color: transparent;\n  transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);\n}\n.navButton[data-side=\"prev\"] { left: 2px; }\n.navButton[data-side=\"next\"] { right: 2px; }\n.navButton:active:not(:disabled) { transform: scale(.92); }\n.navButton:disabled { opacity: .35; cursor: default; }\n\n/* Months slide horizontally inside a clipped window that keeps a little room for the blur. */\n.viewport { position: relative; display: grid; height: calc(60px + var(--cell) * 6); overflow: clip; overflow-clip-margin: 2px; }\n.viewport > * { grid-area: 1 / 1; }\n.months { display: flex; gap: 24px; }\n.month { width: calc(var(--cell) * 7); }\n.monthTitle { height: 32px; margin: 0; color: var(--foreground); font-weight: 500; line-height: 32px; text-align: center; white-space: nowrap; }\n.grid { display: grid; }\n.weekdays { display: grid; height: 28px; align-items: center; color: var(--text-muted); font-size: var(--text-xs); grid-template-columns: repeat(7, var(--cell)); text-align: center; }\n.weeks { position: relative; }\n.week { display: grid; height: var(--cell); grid-template-columns: repeat(7, var(--cell)); }\n.cell, .blank { position: relative; display: block; }\n\n/* The highlight lives under the days: one bar per week plus a solid end at each side of the range. */\n.layer { position: absolute; inset: 0; pointer-events: none; }\n.bar { position: absolute; height: calc(var(--cell) - 6px); border-radius: var(--radius-pill); background: var(--accent-subtle); }\n.thumb { position: absolute; top: 0; left: 0; width: var(--cell); height: var(--cell); }\n.thumb::before { content: \"\"; position: absolute; inset: 3px; border-radius: 50%; background: var(--accent); }\n\n.day {\n  position: relative;\n  z-index: 1;\n  display: grid;\n  width: 100%;\n  height: 100%;\n  place-items: center;\n  padding: 0;\n  border: 0;\n  border-radius: 50%;\n  background: transparent;\n  color: var(--foreground);\n  cursor: pointer;\n  font: inherit;\n  font-variant-numeric: tabular-nums;\n  -webkit-tap-highlight-color: transparent;\n  transition: color var(--duration-fast) var(--ease-standard);\n}\n.day > span { position: relative; display: grid; width: calc(100% - 6px); height: calc(100% - 6px); place-items: center; border-radius: 50%; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }\n.day:active:not(:disabled) > span { transform: scale(.9); }\n.day[data-edge] { color: var(--accent-foreground); font-weight: 500; transition-delay: 40ms; }\n.day[data-today]::after { content: \"\"; position: absolute; bottom: 5px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: var(--accent); }\n.day[data-today][data-edge]::after { background: var(--accent-foreground); }\n.day:disabled { color: var(--text-muted); cursor: default; opacity: .5; }\n.day:focus-visible:not([data-edge]) > span { background: color-mix(in oklab, var(--foreground) 7%, transparent); }\n\n/* Footer: the formatted range and the two decisions. */\n.footer { display: flex; width: 0; min-width: 100%; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; padding: 10px 2px 0 12px; border-top: 1px solid var(--border-subtle); }\n.summary { display: grid; min-width: 0; gap: 1px; }\n.summaryValue { display: inline-flex; width: max-content; font-weight: 500; font-variant-numeric: tabular-nums; }\n.summaryCount { display: inline-flex; min-height: 1.4em; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }\n.actions { display: flex; flex: none; align-items: center; gap: 6px; }\n.ghost, .primary {\n  display: inline-flex;\n  height: var(--control-height-sm);\n  align-items: center;\n  justify-content: center;\n  padding: 0 14px;\n  border: 0;\n  border-radius: var(--radius-control);\n  cursor: pointer;\n  font: inherit;\n  font-weight: 500;\n  white-space: nowrap;\n  -webkit-tap-highlight-color: transparent;\n  transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard);\n}\n.ghost { background: transparent; color: var(--text-secondary); }\n.primary { min-width: 4.75rem; background: var(--accent); color: var(--accent-foreground); }\n.ghost:active, .primary:active:not(:disabled) { transform: scale(.97); }\n.primary:disabled { opacity: .45; cursor: default; }\n.ghost:focus-visible { background: var(--surface-muted); color: var(--foreground); }\n.preset:focus-visible, .navButton:focus-visible { background: color-mix(in oklab, var(--foreground) 5%, transparent); color: var(--foreground); }\n\n.srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }\n\n/* Compact: one month under a scrolling row of presets. */\n.panel[data-compact] { padding: 8px; }\n.panel[data-compact] .body { flex-direction: column; gap: 8px; }\n.panel[data-compact] .rail { width: auto; flex-direction: row; gap: 2px; margin: 0 -8px; padding: 0 8px 8px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); overscroll-behavior-x: contain; scrollbar-width: none; }\n.panel[data-compact] .rail::-webkit-scrollbar { display: none; }\n.panel[data-compact] .preset { padding: 0 11px; }\n.panel[data-compact] .calendars { align-self: center; }\n.panel[data-compact] .footer { flex-wrap: wrap; margin-top: 6px; padding-left: 11px; }\n.panel[data-compact] .actions { margin-left: auto; }\n.panel[data-compact] .summary { flex: 1 1 8rem; }\n\n@media (hover: hover) and (pointer: fine) {\n  .root:not([data-open]):has(> .trigger:hover:not(:disabled)) .surface { background: color-mix(in oklab, var(--foreground) 3%, var(--surface-raised)); }\n  .root:not([data-open]):has(> .trigger:hover:not(:disabled)) .surface::after { border-color: var(--border-strong); }\n  .preset:hover { color: var(--foreground); }\n  .preset:not([aria-pressed=\"true\"]):hover { background: color-mix(in oklab, var(--foreground) 4%, transparent); }\n  .navButton:hover:not(:disabled) { background: color-mix(in oklab, var(--foreground) 6.5%, transparent); color: var(--foreground); }\n  .day:hover:not(:disabled):not([data-edge]) > span { background: color-mix(in oklab, var(--foreground) 7%, transparent); }\n  .ghost:hover { background: var(--surface-muted); color: var(--foreground); }\n  .primary:hover:not(:disabled) { background: var(--accent-strong); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .surface, .preset, .navButton, .day, .day > span, .ghost, .primary { transition-duration: 0ms; }\n}\n\n@media (prefers-contrast: more) {\n  .surface::after { border-color: var(--border-strong); }\n}\n"
    }
  ],
  "docs": "Docs and live preview: https://ui.xzero.co.uk/components/date-range-picker",
  "meta": {
    "tier": "free",
    "kind": "component",
    "tags": [
      "inputs",
      "new"
    ],
    "docs": "https://ui.xzero.co.uk/components/date-range-picker",
    "markdown": "https://ui.xzero.co.uk/components/date-range-picker/markdown"
  }
}