{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "range-date-picker",
  "title": "Range Date Picker",
  "author": "AlgorizeTech",
  "description": "Ask the user to pick one of 4 range date picker variants: https://avaraui.com/view/range-date-picker. This item holds 4 of them as looks of one component (single, priced-single, dual, priced-dual). Skip the question if the user already named one. An interactive Range Date Picker featuring single month, dual-month, and rate-priced calendar layouts with customizable theme colors.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/time-picker.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/range-date-picker.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\n\n/**\n * False while server-rendering and during hydration, true afterwards — without\n * the extra render an effect-flipped useState costs.\n */\nconst subscribeToNothing = () => () => {};\nconst useIsMounted = () =>\n  useSyncExternalStore(\n    subscribeToNothing,\n    () => true,\n    () => false,\n  );\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  CalendarDays,\n  ChevronLeft,\n  ChevronRight,\n  Clock,\n  RotateCcw,\n} from \"lucide-react\";\nimport { TimePicker } from \"./blocks/TimePickerBlock\";\nimport {\n  formatTime,\n  roundToStep,\n  setTime,\n  type TimeFormat,\n} from \"./blocks/time-picker-utils\";\nimport { springLinear } from \"./blocks/motion-curves\";\n\nexport type RangeDatePickerColor =\n  | \"primary\"\n  | \"indigo\"\n  | \"rose\"\n  | \"emerald\"\n  | \"zinc\"\n  | string;\n\nexport interface DateRange {\n  from: Date | null;\n  to: Date | null;\n}\n\nexport interface RangeDatePickerProps {\n  className?: string;\n  variant?: \"single\" | \"dual\" | \"priced-single\" | \"priced-dual\" | \"priced\";\n  showPrices?: boolean;\n  value?: DateRange;\n  onChange?: (range: DateRange) => void;\n  defaultFrom?: Date | null;\n  defaultTo?: Date | null;\n  color?: RangeDatePickerColor;\n  disabled?: boolean;\n  getPrice?: (date: Date) => string | number | null;\n  /** Greys out every day before today so it cannot be picked. */\n  disablePast?: boolean;\n  /** Earliest selectable day (inclusive). Combine with `disablePast` freely — the later bound wins. */\n  minDate?: Date;\n  /** Latest selectable day (inclusive). */\n  maxDate?: Date;\n  /**\n   * Adds Start / End time columns. The card opens with the calendar alone;\n   * picking a start day unfolds the columns beside it (under it when the card\n   * is narrow), and clearing the range folds them away again. Both dates then\n   * carry a time, and the end can never precede the start on the same day.\n   */\n  showTime?: boolean;\n  /** 12-hour clock with AM / PM, or 24-hour. Default \"12h\". */\n  timeFormat?: TimeFormat;\n  /** Minute granularity of the time columns. Default 5. */\n  minuteStep?: number;\n}\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    border: \"border-primary/40\",\n    text: \"text-primary\",\n    bg: \"bg-primary text-primary-foreground\",\n    bgHover: \"hover:bg-primary/90\",\n    rangeBg: \"bg-primary/15 text-foreground\",\n    ring: \"ring-primary/20\",\n    badge: \"bg-primary/10 text-primary border-primary/20\",\n  },\n  indigo: {\n    border: \"border-indigo-500/40\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    bg: \"bg-indigo-600 text-white\",\n    bgHover: \"hover:bg-indigo-700\",\n    rangeBg: \"bg-indigo-500/15 text-indigo-950 dark:text-indigo-100\",\n    ring: \"ring-indigo-500/20\",\n    badge: \"bg-indigo-500/10 text-indigo-600 border-indigo-500/20\",\n  },\n  rose: {\n    border: \"border-rose-500/40\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    bg: \"bg-rose-600 text-white\",\n    bgHover: \"hover:bg-rose-700\",\n    rangeBg: \"bg-rose-500/15 text-rose-950 dark:text-rose-100\",\n    ring: \"ring-rose-500/20\",\n    badge: \"bg-rose-500/10 text-rose-600 border-rose-500/20\",\n  },\n  emerald: {\n    border: \"border-emerald-500/40\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    bg: \"bg-emerald-600 text-white\",\n    bgHover: \"hover:bg-emerald-700\",\n    rangeBg: \"bg-emerald-500/15 text-emerald-950 dark:text-emerald-100\",\n    ring: \"ring-emerald-500/20\",\n    badge: \"bg-emerald-500/10 text-emerald-600 border-emerald-500/20\",\n  },\n  zinc: {\n    border: \"border-zinc-500/40\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    bg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900\",\n    bgHover: \"hover:bg-zinc-800 dark:hover:bg-zinc-200\",\n    rangeBg: \"bg-zinc-500/15 text-zinc-900 dark:text-zinc-100\",\n    ring: \"ring-zinc-500/20\",\n    badge: \"bg-zinc-500/10 text-zinc-800 dark:text-zinc-200 border-zinc-500/20\",\n  },\n};\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];\n\nconst WEEKDAY_NAMES = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\n\nconst subDays = (date: Date, days: number): Date => {\n  const result = new Date(date);\n  result.setDate(result.getDate() - days);\n  return result;\n};\n\nconst addDays = (date: Date, days: number): Date => {\n  const result = new Date(date);\n  result.setDate(result.getDate() + days);\n  return result;\n};\n\nconst isSameDay = (date1: Date | null, date2: Date | null): boolean => {\n  if (!date1 || !date2) return false;\n  return (\n    date1.getFullYear() === date2.getFullYear() &&\n    date1.getMonth() === date2.getMonth() &&\n    date1.getDate() === date2.getDate()\n  );\n};\n\nconst isBetween = (\n  target: Date,\n  start: Date | null,\n  end: Date | null,\n): boolean => {\n  if (!start || !end) return false;\n  const t = new Date(\n    target.getFullYear(),\n    target.getMonth(),\n    target.getDate(),\n  ).getTime();\n  const s = new Date(\n    start.getFullYear(),\n    start.getMonth(),\n    start.getDate(),\n  ).getTime();\n  const e = new Date(\n    end.getFullYear(),\n    end.getMonth(),\n    end.getDate(),\n  ).getTime();\n  const min = Math.min(s, e);\n  const max = Math.max(s, e);\n  return t > min && t < max;\n};\n\nconst formatDateString = (date: Date | null): string => {\n  if (!date) return \"\";\n  const month = MONTH_NAMES[date.getMonth()].slice(0, 3);\n  return `${month} ${date.getDate()}, ${date.getFullYear()}`;\n};\n\nconst startOfDay = (d: Date) =>\n  new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst endOfDay = (d: Date) =>\n  new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);\n\nconst generateMonthDays = (year: number, month: number) => {\n  const firstDayOfWeek = new Date(year, month, 1).getDay();\n  const daysInMonth = new Date(year, month + 1, 0).getDate();\n  const daysInPrevMonth = new Date(year, month, 0).getDate();\n\n  const days: { date: Date; isCurrentMonth: boolean }[] = [];\n\n  for (let i = firstDayOfWeek - 1; i >= 0; i--) {\n    days.push({\n      date: new Date(year, month - 1, daysInPrevMonth - i),\n      isCurrentMonth: false,\n    });\n  }\n\n  for (let d = 1; d <= daysInMonth; d++) {\n    days.push({\n      date: new Date(year, month, d),\n      isCurrentMonth: true,\n    });\n  }\n\n  const remainingCells = 42 - days.length;\n  for (let i = 1; i <= remainingCells; i++) {\n    days.push({\n      date: new Date(year, month + 1, i),\n      isCurrentMonth: false,\n    });\n  }\n\n  return days;\n};\n\n/** Card width with the calendar alone, and with the time rail unfolded beside it. */\nconst CALENDAR_WIDTH = \"24rem\";\nconst DUAL_WIDTH = \"42rem\";\n// Rail column (11rem) + its divider padding (1.25rem) + gap to the calendar (1.5rem):\n// added to the calendar width so the grid keeps its exact size while the rail opens.\nconst CALENDAR_WITH_RAIL_WIDTH = \"37.75rem\";\n/** Below this much room the rail unfolds under the calendar instead of beside it. */\nconst SIDE_RAIL_MIN_PX = 512;\n/** The side rail: its w-44 column (the pl-5 divider padding included) plus the ml-6 gap. */\nconst SIDE_RAIL_WIDTH = \"12.5rem\";\n\n// ------------------------------------------\n// Motion — the same curves as DatePicker so both pickers unfold identically:\n// panels (and the card) unfold on a spring (stiffness 240, damping 30, mass\n// 0.9, drawn as a CSS `linear()` curve) and fold away on a short ease, which\n// also carries the time columns in and the picker / hint swap. Reduced\n// motion: instant.\n// ------------------------------------------\nconst UNFOLD = springLinear({ stiffness: 240, damping: 30, mass: 0.9 });\nconst SETTLE_EASE = \"cubic-bezier(0.22, 1, 0.36, 1)\";\nconst unfoldOn = (property: string) => `${property} ${UNFOLD.duration}ms ${UNFOLD.easing}`;\nconst settleOn = (property: string) => `${property} 0.28s ${SETTLE_EASE}`;\n\nconst STYLES = `\n@keyframes range-picker-col-side { from { opacity: 0; transform: translateX(-12px); } }\n@keyframes range-picker-col-under { from { opacity: 0; transform: translateY(8px); } }\n[data-fold-in] .range-picker-cols > * {\n  animation: 0.28s ${SETTLE_EASE} calc(0.12s + var(--range-picker-i) * 0.07s) backwards;\n}\n[data-fold-in] .range-picker-cols[data-side] > * { animation-name: range-picker-col-side; }\n[data-fold-in] .range-picker-cols:not([data-side]) > * { animation-name: range-picker-col-under; }\n`;\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener(\"change\", onChange);\n  return () => mq.removeEventListener(\"change\", onChange);\n};\n/** The visitor's reduced-motion setting; false while server-rendering. */\nfunction useReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(REDUCED_MOTION).matches,\n    () => false,\n  );\n}\n\ntype FoldPhase = \"closed\" | \"entering\" | \"open\" | \"closing\";\n\n/**\n * A panel that unfolds when `show` turns on and folds away before it leaves:\n * open on the spring, closed on the ease, already open if shown from the\n * start, and still showing what it last showed while it folds. `axis=\"x\"`\n * unfolds its width to `size`; `axis=\"y\"` its height to whatever it holds.\n * Once it has unfolded with motion it carries `data-fold-in`, which is what\n * brings the time columns in after it.\n */\nfunction Fold({\n  show,\n  axis,\n  size,\n  instant,\n  className,\n  children,\n}: {\n  show: boolean;\n  axis: \"x\" | \"y\";\n  size?: number | string;\n  instant: boolean;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const [phase, setPhase] = useState<FoldPhase>(show ? \"open\" : \"closed\");\n  const [cameIn, setCameIn] = useState(false);\n  // What it last showed, kept so the panel still holds it while it folds.\n  const [shown, setShown] = useState(children);\n  if (show && shown !== children) setShown(children);\n\n  // Follow `show` while rendering, so the new phase is what first commits.\n  if (show && phase === \"closed\") {\n    setPhase(instant ? \"open\" : \"entering\");\n    setCameIn(!instant);\n  } else if (show && phase === \"closing\") setPhase(\"open\");\n  else if (!show && (phase === \"open\" || phase === \"entering\")) setPhase(instant ? \"closed\" : \"closing\");\n\n  // Entering: the panel mounts with the folded look; commit it, then\n  // transition to the open one before the first paint.\n  const enter = useCallback((el: HTMLDivElement | null) => {\n    if (!el) return;\n    void el.offsetWidth;\n    setPhase((p) => (p === \"entering\" ? \"open\" : p));\n  }, []);\n\n  // In case the fold never reports its end (a hidden tab, `display: none`).\n  useEffect(() => {\n    if (phase !== \"closing\") return;\n    const t = window.setTimeout(() => setPhase(\"closed\"), 400);\n    return () => window.clearTimeout(t);\n  }, [phase]);\n\n  if (phase === \"closed\") return null;\n\n  const open = phase === \"open\";\n  const dimension = axis === \"x\" ? \"width\" : \"grid-template-rows\";\n  const on = phase === \"closing\" ? settleOn : unfoldOn;\n  return (\n    <div\n      ref={enter}\n      data-fold-in={cameIn || undefined}\n      className={className}\n      style={{\n        ...(axis === \"x\"\n          ? { width: open ? size : 0 }\n          : { display: \"grid\", gridTemplateRows: open ? \"1fr\" : \"0fr\" }),\n        opacity: open ? 1 : 0,\n        transition: instant ? \"none\" : `${on(dimension)}, ${on(\"opacity\")}`,\n      }}\n      onTransitionEnd={(e) => {\n        if (phase === \"closing\" && e.target === e.currentTarget && e.propertyName === dimension) setPhase(\"closed\");\n      }}\n    >\n      {axis === \"x\" ? shown : <div className=\"min-h-0\">{shown}</div>}\n    </div>\n  );\n}\n\ntype SwapPhase = \"from\" | \"shown\" | \"leaving\";\n\n/**\n * One thing that gives way to another, one at a time: the showing one fades\n * up and out, then the next fades up in — the leaving one keeps what it\n * showed. Starts shown, without motion.\n */\nfunction WaitSwap({\n  swapKey,\n  instant,\n  classNames,\n  children,\n}: {\n  swapKey: string;\n  instant: boolean;\n  classNames?: Record<string, string>;\n  children: React.ReactNode;\n}) {\n  const [shownKey, setShownKey] = useState(swapKey);\n  const [phase, setPhase] = useState<SwapPhase>(\"shown\");\n  // What the showing one holds, kept so the leaving one still shows it.\n  const [shown, setShown] = useState(children);\n  if (shownKey === swapKey && shown !== children) setShown(children);\n  const wanted = useRef(swapKey);\n\n  useLayoutEffect(() => {\n    wanted.current = swapKey;\n  }, [swapKey]);\n\n  // Follow `swapKey` while rendering, so the new phase is what first commits.\n  if (swapKey === shownKey) {\n    if (phase === \"leaving\") setPhase(\"shown\");\n  } else if (instant) {\n    setShownKey(swapKey);\n    setPhase(\"shown\");\n  } else if (phase !== \"leaving\") setPhase(\"leaving\");\n\n  // Coming in: commit the starting look, then transition to the resting one.\n  const comeIn = useCallback(\n    (el: HTMLDivElement | null) => {\n      if (!el || phase !== \"from\") return;\n      void el.offsetWidth;\n      setPhase(\"shown\");\n    },\n    [phase],\n  );\n\n  // The leaving one has gone: bring in whatever is wanted now.\n  const settle = useCallback(() => {\n    setShownKey(wanted.current);\n    setPhase(\"from\");\n  }, []);\n\n  // In case the transition never ends (a hidden tab, `display: none`).\n  useEffect(() => {\n    if (phase !== \"leaving\") return;\n    const t = window.setTimeout(settle, 400);\n    return () => window.clearTimeout(t);\n  }, [phase, settle]);\n\n  const y = phase === \"from\" ? 6 : phase === \"leaving\" ? -6 : 0;\n  return (\n    <div\n      key={shownKey}\n      ref={comeIn}\n      className={classNames?.[shownKey]}\n      style={{\n        opacity: phase === \"shown\" ? 1 : 0,\n        transform: y ? `translateY(${y}px)` : \"none\",\n        transition: phase === \"from\" || instant ? \"none\" : `${settleOn(\"opacity\")}, ${settleOn(\"transform\")}`,\n      }}\n      onTransitionEnd={(e) => {\n        if (phase === \"leaving\" && e.target === e.currentTarget && e.propertyName === \"opacity\") settle();\n      }}\n    >\n      {shown}\n    </div>\n  );\n}\n\n/**\n * Width of the element, kept current through a ResizeObserver. Drives the\n * side-vs-stacked decision in JS (rather than a container query) so the\n * unfold can animate the right axis and the card can grow in step with it.\n */\nfunction useElementWidth(ref: React.RefObject<HTMLElement | null>) {\n  const [width, setWidth] = useState(0);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const measure = () => setWidth(el.getBoundingClientRect().width);\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref]);\n  return width;\n}\n\nexport function RangeDatePicker({\n  className,\n  variant = \"single\",\n  showPrices = false,\n  value,\n  onChange,\n  defaultFrom,\n  defaultTo,\n  color = \"primary\",\n  disabled = false,\n  getPrice,\n  disablePast = false,\n  minDate,\n  maxDate,\n  showTime = false,\n  timeFormat = \"12h\",\n  minuteStep = 5,\n}: RangeDatePickerProps) {\n  const mounted = useIsMounted();\n\n  const isDual = variant === \"dual\" || variant === \"priced-dual\";\n  const isPriced =\n    showPrices ||\n    variant === \"priced\" ||\n    variant === \"priced-single\" ||\n    variant === \"priced-dual\" ||\n    getPrice !== undefined;\n\n  // With time columns the card starts empty and the rail unfolds on the first\n  // pick; without them the familiar last-week range is seeded.\n  const [internalFrom, setInternalFrom] = useState<Date | null>(\n    () => defaultFrom ?? (showTime ? null : subDays(new Date(), 7)),\n  );\n  const [internalTo, setInternalTo] = useState<Date | null>(\n    () => defaultTo ?? (showTime ? null : new Date()),\n  );\n\n  const fromDate = value !== undefined ? value.from : internalFrom;\n  const toDate = value !== undefined ? value.to : internalTo;\n\n  const [selectingFrom, setSelectingFrom] = useState<Date | null>(fromDate);\n  const [selectingTo, setSelectingTo] = useState<Date | null>(toDate);\n  const [hoverDate, setHoverDate] = useState<Date | null>(null);\n\n  const [viewDate, setViewDate] = useState<Date>(() => fromDate || new Date());\n\n  const theme = colorThemes[color] || colorThemes.primary;\n\n  const frameRef = useRef<HTMLDivElement>(null);\n  const frameWidth = useElementWidth(frameRef);\n  const reduceMotion = useReducedMotion();\n\n  // Dual month + times stays stacked — three columns would be far too wide.\n  // A single month gets the rail beside it when there is room for the open card.\n  const sideTimes = showTime && !isDual && frameWidth >= SIDE_RAIL_MIN_PX;\n  // The rail exists once a start day is chosen and folds away on Clear.\n  const railOpen = showTime && selectingFrom !== null;\n\n  const formatStamp = (date: Date | null) =>\n    !date\n      ? \"\"\n      : showTime\n        ? `${formatDateString(date)}, ${formatTime(date, timeFormat)}`\n        : formatDateString(date);\n\n  // Adopt a new controlled range as it arrives. Handled during render so\n  // the previous range is never painted first.\n  const [seededRange, setSeededRange] = useState({ fromDate, toDate });\n  if (seededRange.fromDate !== fromDate || seededRange.toDate !== toDate) {\n    setSeededRange({ fromDate, toDate });\n    setSelectingFrom(fromDate);\n    setSelectingTo(toDate);\n  }\n\n  // --- Bounds ---\n  // `disablePast` is sugar for minDate = today; whichever lower bound is later\n  // wins, so both can be passed together without surprises.\n  const today = new Date();\n  const lowerCandidates = [\n    disablePast ? startOfDay(today) : null,\n    minDate ? startOfDay(minDate) : null,\n  ].filter(Boolean) as Date[];\n  const lower = lowerCandidates.length\n    ? new Date(Math.max(...lowerCandidates.map((d) => d.getTime())))\n    : null;\n  const upper = maxDate ? endOfDay(maxDate) : null;\n  const isDayDisabled = (date: Date) =>\n    (lower !== null && endOfDay(date) < lower) ||\n    (upper !== null && startOfDay(date) > upper);\n\n  // Times are compared with each other only when both ends fall on one day,\n  // and with \"now\" only on today when the past is disabled.\n  const sameDay = (a: Date | null, b: Date | null) =>\n    !!a && !!b && isSameDay(a, b);\n  // The start moves freely (an end that would fall behind it is pushed\n  // forward by `handleFromTime`); only the end is fenced by the start.\n  const fromMinTime =\n    disablePast && selectingFrom && isSameDay(selectingFrom, today)\n      ? today\n      : null;\n  const toMinTime = sameDay(selectingFrom, selectingTo)\n    ? selectingFrom\n    : disablePast && selectingTo && isSameDay(selectingTo, today)\n      ? today\n      : null;\n\n  // A clicked day inherits the time already chosen for that end of the range\n  // (or \"now\" rounded to the step), so re-picking a day never resets the clock.\n  const withTime = (day: Date, previous: Date | null) => {\n    if (!showTime) return day;\n    const src = previous ?? roundToStep(today, minuteStep);\n    let next = setTime(day, src.getHours(), src.getMinutes());\n    if (disablePast && isSameDay(next, today) && next < today)\n      next = roundToStep(today, minuteStep);\n    return next;\n  };\n\n  const commitRange = (newFrom: Date | null, newTo: Date | null) => {\n    setSelectingFrom(newFrom);\n    setSelectingTo(newTo);\n    if (value === undefined) {\n      setInternalFrom(newFrom);\n      setInternalTo(newTo);\n    }\n    onChange?.({ from: newFrom, to: newTo });\n  };\n\n  const handleDateClick = (clickedDate: Date) => {\n    if (disabled || isDayDisabled(clickedDate)) return;\n\n    let newFrom: Date | null = null;\n    let newTo: Date | null = null;\n\n    if (!selectingFrom || (selectingFrom && selectingTo)) {\n      newFrom = withTime(clickedDate, selectingFrom);\n      newTo = null;\n    } else if (selectingFrom && !selectingTo) {\n      if (\n        startOfDay(clickedDate).getTime() < startOfDay(selectingFrom).getTime()\n      ) {\n        newFrom = withTime(clickedDate, selectingFrom);\n        newTo = selectingFrom;\n      } else {\n        newFrom = selectingFrom;\n        newTo = withTime(clickedDate, selectingTo);\n        // Same day, end earlier than start: pull the end up to the start.\n        if (isSameDay(newFrom, newTo) && newTo < newFrom)\n          newTo = new Date(newFrom);\n      }\n    }\n\n    commitRange(newFrom, newTo);\n  };\n\n  const handleFromTime = (next: Date) => {\n    let to = selectingTo;\n    if (to && isSameDay(next, to) && to < next) to = new Date(next);\n    commitRange(next, to);\n  };\n  const handleToTime = (next: Date) => commitRange(selectingFrom, next);\n\n  const resetRange = () => {\n    // With time columns, Clear returns to the empty card so the rail folds\n    // away; the plain calendar resets to today as before.\n    if (showTime) {\n      commitRange(null, null);\n      return;\n    }\n    // Reset keeps whatever times were chosen; with the past disabled, today's\n    // start is clamped to now by `withTime`.\n    const from = withTime(startOfDay(today), selectingFrom);\n    const to = isDual\n      ? withTime(addDays(startOfDay(today), 28), selectingTo)\n      : withTime(startOfDay(today), selectingTo);\n    commitRange(from, isSameDay(from, to) && to < from ? new Date(from) : to);\n    setViewDate(new Date(from));\n  };\n\n  // Month Math\n  const year1 = viewDate.getFullYear();\n  const month1 = viewDate.getMonth();\n  const days1 = generateMonthDays(year1, month1);\n\n  // Month 2 (Right calendar for dual variant)\n  const nextMonthDate = new Date(year1, month1 + 1, 1);\n  const year2 = nextMonthDate.getFullYear();\n  const month2 = nextMonthDate.getMonth();\n  const days2 = generateMonthDays(year2, month2);\n\n  const prevMonth = () => setViewDate(new Date(year1, month1 - 1, 1));\n  const nextMonth = () => setViewDate(new Date(year1, month1 + 1, 1));\n\n  const effectiveTo =\n    selectingTo || (selectingFrom && hoverDate ? hoverDate : null);\n  const effectiveFrom =\n    selectingFrom &&\n    effectiveTo &&\n    effectiveTo.getTime() < selectingFrom.getTime()\n      ? effectiveTo\n      : selectingFrom;\n  const effectiveEnd =\n    selectingFrom &&\n    effectiveTo &&\n    effectiveTo.getTime() < selectingFrom.getTime()\n      ? selectingFrom\n      : effectiveTo;\n\n  // The frame is the space the card may take. Its width never animates and is\n  // measured for the side-or-stacked call; it wraps the skeleton too so the\n  // observed node survives the switch to the real card.\n  const frameClass = cn(\n    \"w-full mx-auto\",\n    isDual ? \"max-w-2xl\" : showTime ? \"max-w-[37.75rem]\" : \"max-w-sm\",\n  );\n\n  if (!mounted) {\n    return (\n      <div ref={frameRef} className={frameClass}>\n        <div\n          className={cn(\n            \"w-full rounded-2xl bg-card border border-border/80 p-5 animate-pulse mx-auto\",\n            isDual ? \"max-w-2xl h-96\" : \"max-w-sm h-80\",\n            isPriced && \"h-[420px]\",\n            className,\n          )}\n        />\n      </div>\n    );\n  }\n\n  const renderSingleMonthGrid = (\n    y: number,\n    m: number,\n    days: { date: Date; isCurrentMonth: boolean }[],\n    showPrevNav: boolean,\n    showNextNav: boolean,\n  ) => (\n    <div className=\"flex-1 flex flex-col justify-between min-w-0\">\n      {/* Month Header */}\n      <header className=\"flex items-center justify-between pb-3\">\n        <h2 className=\"text-sm font-extrabold text-foreground tracking-tight\">\n          {MONTH_NAMES[m]} {y}\n        </h2>\n        <div className=\"flex items-center gap-1\">\n          {showPrevNav && (\n            <button\n              type=\"button\"\n              onClick={prevMonth}\n              className=\"size-7 rounded-lg border border-border/80 hover:bg-muted flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <ChevronLeft className=\"size-4\" />\n            </button>\n          )}\n          {showNextNav && (\n            <button\n              type=\"button\"\n              onClick={nextMonth}\n              className=\"size-7 rounded-lg border border-border/80 hover:bg-muted flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <ChevronRight className=\"size-4\" />\n            </button>\n          )}\n        </div>\n      </header>\n\n      {/* Weekdays Header */}\n      <section className=\"grid grid-cols-7 gap-1 text-center mb-2\">\n        {WEEKDAY_NAMES.map((day) => (\n          <span key={day} className=\"text-xs font-semibold text-muted-foreground\">\n            {day}\n          </span>\n        ))}\n      </section>\n\n      {/* Days Grid */}\n      <section className=\"grid grid-cols-7 gap-y-1.5 gap-x-0 text-center\">\n        {days.map(({ date, isCurrentMonth }, idx) => {\n          const isStart = isSameDay(date, effectiveFrom);\n          const isEnd = isSameDay(date, effectiveEnd);\n          const isInRange = isBetween(date, effectiveFrom, effectiveEnd);\n          const isOff = isDayDisabled(date);\n\n          const price = isCurrentMonth\n            ? getPrice\n              ? getPrice(date)\n              : isPriced\n                ? date.getDay() === 0 || date.getDay() === 6\n                  ? \"$120\"\n                  : \"$100\"\n                : null\n            : null;\n\n          return (\n            <div\n              key={idx}\n              onMouseEnter={() => {\n                if (!isOff) setHoverDate(date);\n              }}\n              className={cn(\n                \"relative flex items-center justify-center my-0.5\",\n                isPriced ? \"h-12\" : \"h-8\",\n                isInRange && theme.rangeBg,\n                isStart && effectiveEnd && \"rounded-l-xl\",\n                isEnd && effectiveFrom && \"rounded-r-xl\",\n              )}\n            >\n              <button\n                type=\"button\"\n                disabled={isOff}\n                aria-disabled={isOff}\n                onClick={() => handleDateClick(date)}\n                className={cn(\n                  \"w-full rounded-xl transition-all duration-100 flex flex-col items-center justify-center z-10 px-1 py-1\",\n                  isPriced ? \"h-11\" : \"size-8 rounded-lg\",\n                  !isCurrentMonth && \"text-muted-foreground/30\",\n                  isCurrentMonth &&\n                    !isStart &&\n                    !isEnd &&\n                    \"hover:bg-muted text-foreground\",\n                  (isStart || isEnd) &&\n                    cn(\n                      theme.bg,\n                      theme.bgHover,\n                      \"font-extrabold scale-105 shadow-xs\",\n                    ),\n                  isOff &&\n                    \"text-muted-foreground/40 cursor-not-allowed hover:bg-transparent\",\n                )}\n              >\n                <span\n                  className={cn(\n                    \"text-xs font-semibold leading-tight\",\n                    (isStart || isEnd) && \"text-primary-foreground\",\n                    isOff && \"line-through font-normal\",\n                  )}\n                >\n                  {date.getDate()}\n                </span>\n                {price && (\n                  <span\n                    className={cn(\n                      \"text-[10px] font-semibold leading-tight mt-0.5 tracking-tight\",\n                      isStart || isEnd\n                        ? \"text-primary-foreground/90 font-semibold\"\n                        : \"text-muted-foreground\",\n                    )}\n                  >\n                    {price}\n                  </span>\n                )}\n              </button>\n            </div>\n          );\n        })}\n      </section>\n    </div>\n  );\n\n  const timeEnds = [\n    {\n      key: \"from\",\n      label: \"Start\",\n      date: selectingFrom,\n      min: fromMinTime,\n      onChange: handleFromTime,\n    },\n    {\n      key: \"to\",\n      label: \"End\",\n      date: selectingTo,\n      min: toMinTime,\n      onChange: handleToTime,\n    },\n  ] as const;\n\n  /** One Start / End column: label, chosen time, and the picker or a hint. */\n  const renderTimeColumn = (end: (typeof timeEnds)[number]) => (\n    <div className=\"flex flex-col gap-2 min-w-0\">\n      <div className=\"flex items-center justify-between\">\n        <span className=\"flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">\n          <Clock className=\"size-3.5\" /> {end.label}\n        </span>\n        <span\n          className={cn(\n            \"text-xs font-semibold tabular-nums\",\n            end.date ? theme.text : \"text-muted-foreground\",\n          )}\n        >\n          {end.date ? formatTime(end.date, timeFormat) : \"--:--\"}\n        </span>\n      </div>\n      <WaitSwap\n        swapKey={end.date ? \"picker\" : \"hint\"}\n        instant={reduceMotion}\n        classNames={{\n          hint: \"h-28 rounded-xl border border-dashed border-border/80 flex flex-col items-center justify-center gap-1.5 px-3 text-center\",\n        }}\n      >\n        {end.date ? (\n          <TimePicker\n            value={end.date}\n            onChange={end.onChange}\n            format={timeFormat}\n            minuteStep={minuteStep}\n            minTime={end.min}\n            maxTime={null}\n            color={color}\n            disabled={disabled}\n            className=\"h-28\"\n          />\n        ) : (\n          <>\n            <CalendarDays className=\"size-4 text-muted-foreground/70\" />\n            <span className=\"text-[11px] font-medium leading-snug text-muted-foreground\">\n              Pick an end day on the calendar\n            </span>\n          </>\n        )}\n      </WaitSwap>\n    </div>\n  );\n\n  /** The two columns, stacked in the rail or side by side under the calendar. */\n  const timeColumns = (stacked: boolean) => (\n    // Each column comes in after the rail has started to unfold (see STYLES).\n    <div\n      data-side={stacked || undefined}\n      className={cn(\n        \"range-picker-cols grid gap-4\",\n        stacked ? \"grid-cols-1\" : \"grid-cols-1 sm:grid-cols-2\",\n      )}\n    >\n      {timeEnds.map((end, i) => (\n        <div key={end.key} style={{ \"--range-picker-i\": i } as React.CSSProperties}>\n          {renderTimeColumn(end)}\n        </div>\n      ))}\n    </div>\n  );\n\n  return (\n    <div ref={frameRef} className={frameClass}>\n      <article\n        style={{\n          maxWidth: isDual\n            ? DUAL_WIDTH\n            : sideTimes && railOpen\n              ? CALENDAR_WITH_RAIL_WIDTH\n              : CALENDAR_WIDTH,\n          transition: reduceMotion ? \"none\" : unfoldOn(\"max-width\"),\n        }}\n        className={cn(\n          \"w-full rounded-2xl bg-card border border-border/80 p-5 flex flex-col gap-4 text-foreground select-none overflow-hidden shadow-none mx-auto my-auto\",\n          disabled && \"opacity-60 pointer-events-none\",\n          className,\n        )}\n      >\n        <style>{STYLES}</style>\n        {/* Calendar + time rail: the rail unfolds to the RIGHT of a single month\n          when there is room, and DOWN from under the calendar otherwise. */}\n        <div className={cn(\"w-full flex\", sideTimes ? \"flex-row\" : \"flex-col\")}>\n          {/* Calendars Grid Area */}\n          <div\n            className={cn(\n              \"w-full flex flex-col gap-6 min-w-0\",\n              isDual && \"sm:flex-row sm:gap-8\",\n            )}\n          >\n            {/* Left Calendar (Month 1) */}\n            {renderSingleMonthGrid(year1, month1, days1, true, !isDual)}\n\n            {/* Right Calendar (Month 2 - Dual Variant Only) */}\n            {isDual && (\n              <div className=\"border-t border-border/60 pt-6 sm:border-t-0 sm:pt-0 sm:border-l sm:pl-8 flex-1\">\n                {renderSingleMonthGrid(year2, month2, days2, false, true)}\n              </div>\n            )}\n          </div>\n\n          {/* Start / End time rail */}\n          <Fold\n            show={railOpen && sideTimes}\n            axis=\"x\"\n            size={SIDE_RAIL_WIDTH}\n            instant={reduceMotion}\n            className=\"shrink-0 overflow-hidden\"\n          >\n            {/* Fixed-width column so the lists are never squeezed mid-unfold. */}\n            <div className=\"w-44 ml-6 pl-5 border-l border-border/60 h-full\">\n              {timeColumns(true)}\n            </div>\n          </Fold>\n          <Fold show={railOpen && !sideTimes} axis=\"y\" instant={reduceMotion} className=\"overflow-hidden\">\n            <div className=\"pt-4 mt-6 border-t border-border/60\">\n              {timeColumns(false)}\n            </div>\n          </Fold>\n        </div>\n\n        {/* Bottom Footer Status Bar */}\n        <footer className=\"flex items-center justify-between pt-3 mt-1 border-t border-border/60\">\n          <div className=\"text-[11px] font-medium text-muted-foreground truncate max-w-[280px]\">\n            {selectingFrom &&\n            selectingTo &&\n            (showTime || !isSameDay(selectingFrom, selectingTo)) ? (\n              <span className=\"flex items-center gap-1.5\">\n                <span className={cn(\"size-2 rounded-full\", theme.bg)} />\n                <strong className=\"text-foreground\">\n                  {formatStamp(selectingFrom)}\n                </strong>{\" \"}\n                –{\" \"}\n                <strong className=\"text-foreground\">\n                  {formatStamp(selectingTo)}\n                </strong>\n              </span>\n            ) : selectingFrom ? (\n              <span className=\"flex items-center gap-1.5\">\n                <span className={cn(\"size-2 rounded-full\", theme.bg)} />\n                <strong className=\"text-foreground\">\n                  {formatStamp(selectingFrom)}\n                </strong>\n                {showTime && <span>– choose an end day</span>}\n              </span>\n            ) : (\n              <span>\n                {showTime ? \"Choose a start day\" : \"Select date range\"}\n              </span>\n            )}\n          </div>\n\n          {(selectingFrom || selectingTo) && (\n            <button\n              type=\"button\"\n              onClick={resetRange}\n              className=\"flex items-center gap-1 px-2.5 py-1 rounded-lg border border-border/80 text-[11px] font-semibold text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\n            >\n              <RotateCcw className=\"size-3\" />\n              {showTime ? \"Clear\" : \"Reset\"}\n            </button>\n          )}\n        </footer>\n      </article>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/custom/range-date-picker.tsx"
    },
    {
      "path": "src/components/custom/blocks/motion-curves.ts",
      "content": "/**\n * Motion curves with no animation library behind them.\n *\n * The components here ship as CSS, GSAP or Motion builds that must move\n * identically, so the curves are computed once, from the same numbers, and\n * handed to whichever engine runs them:\n *\n *   - `cubicBezier(x1, y1, x2, y2)` → an easing function (GSAP accepts any\n *     `(progress) => value` as `ease`), exact for Motion's `ease: [..]` arrays\n *     and for CSS `cubic-bezier()`.\n *   - `spring({ stiffness, damping, mass })` → the analytic solution Motion\n *     Motion uses, as an easing function plus the time it takes to settle, or\n *     as a CSS `linear()` string for a fixed start and end.\n *   - `createSpringFollower` → a value that chases a moving target through the\n *     same spring every frame, like Motion's `useSpring`, for pointer-driven\n *     motion that no fixed curve can describe.\n */\n\n// ---------------------------------------------------------------------------\n// Cubic bezier\n// ---------------------------------------------------------------------------\n\n/**\n * The CSS / Motion cubic-bezier as a function of progress. Newton-Raphson\n * with a bisection fallback, the same solver browsers use.\n */\nexport function cubicBezier(x1: number, y1: number, x2: number, y2: number) {\n  if (x1 === y1 && x2 === y2) return (t: number) => t;\n\n  const a = (p1: number, p2: number) => 1 - 3 * p2 + 3 * p1;\n  const b = (p1: number, p2: number) => 3 * p2 - 6 * p1;\n  const c = (p1: number) => 3 * p1;\n  const at = (t: number, p1: number, p2: number) =>\n    ((a(p1, p2) * t + b(p1, p2)) * t + c(p1)) * t;\n  const slope = (t: number, p1: number, p2: number) =>\n    3 * a(p1, p2) * t * t + 2 * b(p1, p2) * t + c(p1);\n\n  const solveX = (x: number) => {\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const s = slope(t, x1, x2);\n      if (Math.abs(s) < 1e-6) break;\n      const err = at(t, x1, x2) - x;\n      if (Math.abs(err) < 1e-7) return t;\n      t -= err / s;\n    }\n    let lo = 0;\n    let hi = 1;\n    t = x;\n    for (let i = 0; i < 30 && hi - lo > 1e-7; i++) {\n      const value = at(t, x1, x2);\n      if (value < x) lo = t;\n      else hi = t;\n      t = (lo + hi) / 2;\n    }\n    return t;\n  };\n\n  return (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : at(solveX(x), y1, y2));\n}\n\n/** Motion's named eases, as the cubic-beziers they are. */\nexport const MOTION_EASE = {\n  linear: [0, 0, 1, 1],\n  easeIn: [0.42, 0, 1, 1],\n  easeOut: [0, 0, 0.58, 1],\n  easeInOut: [0.42, 0, 0.58, 1],\n  circIn: [0.55, 0, 1, 0.45],\n  circOut: [0.55, 0, 0.45, 1],\n  circInOut: [0.85, 0, 0.15, 1],\n  backIn: [0.31, 0.01, 0.66, -0.59],\n  backOut: [0.33, 1.53, 0.69, 0.99],\n  anticipate: [0.66, -0.56, 0.34, 1.35],\n} as const;\n\n/**\n * Motion's default layout transition (`layout` / `layoutId`):\n * `{ duration: 0.45, ease: [0.4, 0, 0.1, 1] }`.\n */\nexport const LAYOUT_TRANSITION = { duration: 0.45, ease: [0.4, 0, 0.1, 1] as const };\n\n// ---------------------------------------------------------------------------\n// Spring\n// ---------------------------------------------------------------------------\n\nexport interface SpringOptions {\n  /** Motion default 100. */\n  stiffness?: number;\n  /** Motion default 10. */\n  damping?: number;\n  /** Motion default 1. */\n  mass?: number;\n  /** Starting velocity, in units of the whole distance per second. */\n  velocity?: number;\n  /** How close to the end counts as settled, as a fraction of the distance. */\n  restDelta?: number;\n}\n\n/**\n * Normalised position (0 → 1) of a spring released from 0 towards 1, at time\n * `t` seconds. The closed-form solution Motion's `spring()` generator\n * uses for the under-, critically and over-damped cases.\n */\nexport function springAt(options: SpringOptions = {}) {\n  const { stiffness = 100, damping = 10, mass = 1, velocity = 0 } = options;\n  const zeta = damping / (2 * Math.sqrt(stiffness * mass));\n  const omega0 = Math.sqrt(stiffness / mass);\n  // Motion's convention: distance still to cover, velocity toward it negated.\n  const delta = 1;\n  const v0 = -velocity;\n\n  if (zeta < 1) {\n    const omegaD = omega0 * Math.sqrt(1 - zeta * zeta);\n    return (t: number) => {\n      const envelope = Math.exp(-zeta * omega0 * t);\n      return (\n        1 -\n        envelope *\n          (((v0 + zeta * omega0 * delta) / omegaD) * Math.sin(omegaD * t) +\n            delta * Math.cos(omegaD * t))\n      );\n    };\n  }\n  if (zeta === 1) {\n    return (t: number) => 1 - Math.exp(-omega0 * t) * (delta + (v0 + omega0 * delta) * t);\n  }\n  const omegaD = omega0 * Math.sqrt(zeta * zeta - 1);\n  return (t: number) => {\n    const envelope = Math.exp(-zeta * omega0 * t);\n    const freq = Math.min(omegaD * t, 300);\n    return (\n      1 -\n      (envelope *\n        ((v0 + zeta * omega0 * delta) * Math.sinh(freq) + omegaD * delta * Math.cosh(freq))) /\n        omegaD\n    );\n  };\n}\n\n/** Seconds until the spring stays within `restDelta` of its end. */\nexport function springDuration(options: SpringOptions = {}) {\n  const at = springAt(options);\n  const restDelta = options.restDelta ?? 0.001;\n  const step = 1 / 240;\n  let lastOutside = 0;\n  for (let t = 0; t <= 10; t += step) {\n    if (Math.abs(1 - at(t)) > restDelta) lastOutside = t;\n    else if (t - lastOutside > 0.1) break;\n  }\n  return Math.max(lastOutside + step, step);\n}\n\n/**\n * A spring as a duration and an easing function over it — what GSAP takes:\n *\n *   const { duration, ease } = springEase({ stiffness: 300, damping: 20 });\n *   gsap.to(el, { y: 0, duration, ease });\n *\n * Exact for a fixed start and end; a spring that has to react mid-flight to a\n * new target needs `createSpringFollower` instead.\n */\nexport function springEase(options: SpringOptions = {}) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  return {\n    duration,\n    ease: (progress: number) => (progress >= 1 ? 1 : at(progress * duration)),\n  };\n}\n\n/**\n * A spring as a CSS `linear()` easing and its duration in ms, for\n * `transition` / `animation` with a fixed start and end.\n *\n *   const { easing, duration } = springLinear({ stiffness: 400, damping: 30 });\n *   el.style.transition = `transform ${duration}ms ${easing}`;\n */\nexport function springLinear(options: SpringOptions = {}, pointsPerSecond = 60) {\n  const duration = springDuration(options);\n  const at = springAt(options);\n  const count = Math.max(Math.ceil(duration * pointsPerSecond), 8);\n  const points: string[] = [];\n  for (let i = 0; i <= count; i++) {\n    const t = (i / count) * duration;\n    points.push(String(Math.round((i === count ? 1 : at(t)) * 10000) / 10000));\n  }\n  return { easing: `linear(${points.join(\", \")})`, duration: Math.round(duration * 1000) };\n}\n\n// ---------------------------------------------------------------------------\n// Spring follower (useSpring equivalent)\n// ---------------------------------------------------------------------------\n\n/** Something that calls back once per frame with the time since the last, in ms. */\nexport interface FrameTicker {\n  add: (callback: (deltaMs: number) => void) => () => void;\n}\n\n/** requestAnimationFrame as a ticker — the library-free default. */\nexport const rafTicker: FrameTicker = {\n  add(callback) {\n    let last = performance.now();\n    let frame = requestAnimationFrame(function tick(now) {\n      callback(now - last);\n      last = now;\n      frame = requestAnimationFrame(tick);\n    });\n    return () => cancelAnimationFrame(frame);\n  },\n};\n\nexport interface SpringFollower {\n  /** Chase a new target from wherever the value is now, keeping its velocity. */\n  set: (target: number) => void;\n  /** Move to a value instantly, with no motion. */\n  jump: (value: number) => void;\n  get: () => number;\n  destroy: () => void;\n}\n\n/**\n * A value that follows its target through a spring, integrating the same\n * equation of motion Motion's `useSpring` solves — so a pointer-driven spring\n * reads the same in every build. Runs only while moving; `onUpdate` receives\n * each new value.\n */\nexport function createSpringFollower(\n  initial: number,\n  onUpdate: (value: number) => void,\n  options: SpringOptions & { restSpeed?: number; ticker?: FrameTicker } = {},\n): SpringFollower {\n  const { stiffness = 100, damping = 10, mass = 1, ticker = rafTicker } = options;\n  const restDelta = options.restDelta ?? 0.01;\n  const restSpeed = options.restSpeed ?? 0.01;\n  let value = initial;\n  let target = initial;\n  let velocity = 0;\n  let stop: (() => void) | null = null;\n\n  const step = (deltaMs: number) => {\n    // Fixed 1ms sub-steps keep the integration stable for stiff springs and\n    // independent of the frame rate.\n    const steps = Math.min(Math.max(Math.round(deltaMs), 1), 250);\n    for (let i = 0; i < steps; i++) {\n      const force = -stiffness * (value - target) - damping * velocity;\n      velocity += (force / mass) * 0.001;\n      value += velocity * 0.001;\n    }\n    if (Math.abs(target - value) < restDelta && Math.abs(velocity) < restSpeed) {\n      value = target;\n      velocity = 0;\n      stop?.();\n      stop = null;\n    }\n    onUpdate(value);\n  };\n\n  return {\n    set(next) {\n      target = next;\n      if (!stop && value !== target) stop = ticker.add(step);\n    },\n    jump(next) {\n      stop?.();\n      stop = null;\n      value = target = next;\n      velocity = 0;\n      onUpdate(value);\n    },\n    get: () => value,\n    destroy() {\n      stop?.();\n      stop = null;\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/motion-curves.ts"
    },
    {
      "path": "src/components/custom/blocks/time-picker-utils.ts",
      "content": "export type TimeFormat = \"12h\" | \"24h\";\n\n/** A new Date on the same day as `base` at the given time. */\nexport const setTime = (base: Date, hours: number, minutes: number) => {\n  const next = new Date(base);\n  next.setHours(hours, minutes, 0, 0);\n  return next;\n};\n\n/** Rounds `date` up to the next multiple of `step` minutes — \"now\" for a picker default. */\nexport const roundToStep = (date: Date, step: number) => {\n  const next = new Date(date);\n  const m = Math.ceil(next.getMinutes() / step) * step;\n  next.setMinutes(m, 0, 0);\n  return next;\n};\n\nexport const formatTime = (date: Date, format: TimeFormat = \"12h\") =>\n  new Intl.DateTimeFormat(\"en-US\", {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    hour12: format === \"12h\",\n  }).format(date);\n",
      "type": "registry:component",
      "target": "components/custom/blocks/time-picker-utils.ts"
    }
  ],
  "docs": "Named export: RangeDatePicker. Supports variant=('single' | 'dual' | 'priced-single' | 'priced-dual'), getPrice, color, value={from, to}, onChange, disablePast / minDate / maxDate. `showTime` adds Start / End time columns: the card opens as a bare calendar (empty when uncontrolled) and a time rail unfolds beside it on the first pick — under it when narrow — and folds away on Clear; `timeFormat` (12h | 24h) and `minuteStep` tune the columns. Installs time-picker alongside.",
  "categories": [
    "forms",
    "utilities"
  ],
  "type": "registry:component"
}
