{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "type": "registry:ui",
  "title": "Date Range Picker (AD)",
  "description": "A date range picker (Gregorian/AD) with quick presets, two-month view, hover preview, and a minimum date option.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/ui/date-range-picker.tsx",
      "type": "registry:ui",
      "target": "~/components/ui/date-range-picker.tsx",
      "content": "\"use client\";\n\nimport { useState, useRef, useEffect, useMemo, useCallback } from \"react\";\nimport { Calendar, ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DateRange {\n  from: string; // 'YYYY-MM-DD'\n  to: string; // 'YYYY-MM-DD'\n}\n\ninterface DateRangePickerProps {\n  value: DateRange;\n  onChange: (value: DateRange) => void;\n  placeholder?: string;\n  className?: string;\n  min?: string; // 'YYYY-MM-DD'\n  id?: string;\n}\n\nfunction toLocalDate(value: string): Date | null {\n  if (!value) return null;\n  const [y, m, d] = value.split(\"-\").map(Number);\n  return new Date(y, m - 1, d);\n}\n\nfunction toValue(date: Date): string {\n  const y = date.getFullYear();\n  const m = String(date.getMonth() + 1).padStart(2, \"0\");\n  const d = String(date.getDate()).padStart(2, \"0\");\n  return `${y}-${m}-${d}`;\n}\n\nfunction addDays(date: Date, days: number): Date {\n  const d = new Date(date);\n  d.setDate(d.getDate() + days);\n  return d;\n}\n\nfunction addMonths(date: Date, months: number): Date {\n  return new Date(date.getFullYear(), date.getMonth() + months, 1);\n}\n\n// Monday-start week helpers\nfunction startOfWeek(date: Date): Date {\n  const d = new Date(date);\n  const day = d.getDay(); // 0 = Sun ... 6 = Sat\n  const diff = day === 0 ? -6 : 1 - day;\n  return addDays(d, diff);\n}\n\nfunction endOfWeek(date: Date): Date {\n  return addDays(startOfWeek(date), 6);\n}\n\nfunction startOfMonth(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth(), 1);\n}\n\nfunction endOfMonth(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth() + 1, 0);\n}\n\nfunction startOfYear(date: Date): Date {\n  return new Date(date.getFullYear(), 0, 1);\n}\n\nfunction endOfYear(date: Date): Date {\n  return new Date(date.getFullYear(), 11, 31);\n}\n\nfunction isSameDay(a: Date | null, b: Date | null): boolean {\n  if (!a || !b) return false;\n  return (\n    a.getFullYear() === b.getFullYear() &&\n    a.getMonth() === b.getMonth() &&\n    a.getDate() === b.getDate()\n  );\n}\n\nfunction stripTime(date: Date): Date {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\nfunction formatDisplay(date: Date): string {\n  const d = String(date.getDate()).padStart(2, \"0\");\n  const m = String(date.getMonth() + 1).padStart(2, \"0\");\n  const y = date.getFullYear();\n  return `${d}/${m}/${y}`;\n}\n\nconst WEEKDAYS = [\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"];\n\ninterface Preset {\n  label: string;\n  range: () => DateRange;\n}\n\nfunction buildPresets(today: Date): Preset[] {\n  const t = stripTime(today);\n  const yesterday = addDays(t, -1);\n  const lastWeekAnchor = addDays(t, -7);\n  const lastMonthAnchor = new Date(t.getFullYear(), t.getMonth() - 1, 1);\n  const lastYearAnchor = new Date(t.getFullYear() - 1, 0, 1);\n\n  return [\n    { label: \"Today\", range: () => ({ from: toValue(t), to: toValue(t) }) },\n    {\n      label: \"Yesterday\",\n      range: () => ({ from: toValue(yesterday), to: toValue(yesterday) }),\n    },\n    {\n      label: \"This week\",\n      range: () => ({\n        from: toValue(startOfWeek(t)),\n        to: toValue(endOfWeek(t)),\n      }),\n    },\n    {\n      label: \"Last week\",\n      range: () => ({\n        from: toValue(startOfWeek(lastWeekAnchor)),\n        to: toValue(endOfWeek(lastWeekAnchor)),\n      }),\n    },\n    {\n      label: \"Past two weeks\",\n      range: () => ({\n        from: toValue(addDays(startOfWeek(t), -7)),\n        to: toValue(endOfWeek(t)),\n      }),\n    },\n    {\n      label: \"This month\",\n      range: () => ({\n        from: toValue(startOfMonth(t)),\n        to: toValue(endOfMonth(t)),\n      }),\n    },\n    {\n      label: \"Last month\",\n      range: () => ({\n        from: toValue(startOfMonth(lastMonthAnchor)),\n        to: toValue(endOfMonth(lastMonthAnchor)),\n      }),\n    },\n    {\n      label: \"This year\",\n      range: () => ({\n        from: toValue(startOfYear(t)),\n        to: toValue(endOfYear(t)),\n      }),\n    },\n    {\n      label: \"Last year\",\n      range: () => ({\n        from: toValue(startOfYear(lastYearAnchor)),\n        to: toValue(endOfYear(lastYearAnchor)),\n      }),\n    },\n  ];\n}\n\nfunction MonthGrid({\n  viewDate,\n  from,\n  to,\n  hoverDate,\n  minDate,\n  onSelect,\n  onHover,\n  className,\n}: {\n  viewDate: Date;\n  from: Date | null;\n  to: Date | null;\n  hoverDate: Date | null;\n  minDate: Date | null;\n  onSelect: (date: Date) => void;\n  onHover: (date: Date | null) => void;\n  className?: string;\n}) {\n  const year = viewDate.getFullYear();\n  const month = viewDate.getMonth();\n  const rawOffset = new Date(year, month, 1).getDay();\n  const startOffset = rawOffset === 0 ? 6 : rawOffset - 1;\n  const daysInMonth = new Date(year, month + 1, 0).getDate();\n  const today = new Date();\n\n  const prevMonthDays = new Date(year, month, 0).getDate();\n  const cells: { date: Date; inMonth: boolean }[] = [];\n\n  for (let i = startOffset - 1; i >= 0; i--) {\n    cells.push({\n      date: new Date(year, month - 1, prevMonthDays - i),\n      inMonth: false,\n    });\n  }\n  for (let d = 1; d <= daysInMonth; d++) {\n    cells.push({ date: new Date(year, month, d), inMonth: true });\n  }\n  while (cells.length % 7 !== 0) {\n    const last = cells[cells.length - 1].date;\n    cells.push({ date: addDays(last, 1), inMonth: false });\n  }\n\n  const effectiveEnd = to ?? hoverDate;\n\n  const monthLabel = viewDate.toLocaleDateString(\"en-US\", {\n    month: \"long\",\n    year: \"numeric\",\n  });\n\n  return (\n    <div className={cn(\"min-w-0 flex-1\", className)}>\n      <div className=\"mb-2 text-center text-sm font-medium text-zinc-900\">\n        {monthLabel}\n      </div>\n      <div className=\"mb-1 grid grid-cols-7 gap-1\">\n        {WEEKDAYS.map((d) => (\n          <div\n            key={d}\n            className=\"py-1 text-center text-[11px] font-medium text-zinc-500\"\n          >\n            {d}\n          </div>\n        ))}\n      </div>\n      <div className=\"grid grid-cols-7 gap-1\">\n        {cells.map(({ date, inMonth }, i) => {\n          const isDisabled = minDate\n            ? stripTime(date) < stripTime(minDate)\n            : false;\n          const isToday = isSameDay(date, today);\n          const isStart = isSameDay(date, from);\n          const isEnd = isSameDay(date, to);\n          const isBoundary = isStart || isEnd;\n\n          let inRange = false;\n          if (from && effectiveEnd) {\n            const lo = from < effectiveEnd ? from : effectiveEnd;\n            const hi = from < effectiveEnd ? effectiveEnd : from;\n            inRange = date >= stripTime(lo) && date <= stripTime(hi);\n          }\n\n          return (\n            <button\n              key={i}\n              type=\"button\"\n              disabled={isDisabled}\n              onMouseEnter={() => onHover(date)}\n              onClick={() => onSelect(date)}\n              className={cn(\n                \"relative flex h-8 w-8 items-center justify-center rounded-sm text-sm transition-colors\",\n                !inMonth && \"text-zinc-400\",\n                isDisabled && \"cursor-not-allowed text-zinc-300\",\n                !isDisabled &&\n                  inMonth &&\n                  !isBoundary &&\n                  (inRange\n                    ? \"bg-zinc-100 text-zinc-900\"\n                    : isToday\n                      ? \"bg-zinc-100 font-medium text-zinc-900\"\n                      : \"text-zinc-900 hover:bg-zinc-100\"),\n                isBoundary &&\n                  !isDisabled &&\n                  \"bg-zinc-900 font-semibold text-white hover:bg-zinc-900\",\n              )}\n            >\n              {date.getDate()}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport function DateRangePicker({\n  value,\n  onChange,\n  placeholder = \"Select date range\",\n  className,\n  min,\n  id,\n}: DateRangePickerProps) {\n  const minDate = toLocalDate(min || \"\");\n  const [open, setOpen] = useState(false);\n  const [monthNav, setMonthNav] = useState(0);\n  const [hoverDate, setHoverDate] = useState<Date | null>(null);\n  const [draft, setDraft] = useState<DateRange>(value);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const [dropdownPos, setDropdownPos] = useState<{\n    top: number;\n    left: number;\n  } | null>(null);\n\n  const updatePosition = useCallback(() => {\n    if (!triggerRef.current) return;\n    const rect = triggerRef.current.getBoundingClientRect();\n    const width = Math.min(720, window.innerWidth - 16);\n    let left = Math.min(rect.left, window.innerWidth - width - 8);\n    left = Math.max(8, left);\n\n    let top = rect.bottom + 6;\n    if (dropdownRef.current) {\n      const height = dropdownRef.current.offsetHeight;\n      if (top + height > window.innerHeight - 8) {\n        top = Math.max(8, rect.top - height - 6);\n      }\n    }\n\n    setDropdownPos({ top, left });\n  }, []);\n\n  const prevOpen = useRef(open);\n  useEffect(() => {\n    if (open && !prevOpen.current) setDraft(value);\n    prevOpen.current = open;\n  }, [open, value]);\n\n  useEffect(() => {\n    if (!open) return;\n    const handleReposition = () => updatePosition();\n    window.addEventListener(\"scroll\", handleReposition, true);\n    window.addEventListener(\"resize\", handleReposition);\n    const t = window.setTimeout(updatePosition, 0);\n    return () => {\n      window.removeEventListener(\"scroll\", handleReposition, true);\n      window.removeEventListener(\"resize\", handleReposition);\n      window.clearTimeout(t);\n    };\n  }, [open, updatePosition]);\n\n  const from = toLocalDate(draft.from);\n  const to = toLocalDate(draft.to);\n  const displayFrom = toLocalDate(value.from);\n  const displayTo = toLocalDate(value.to);\n\n  const leftView = useMemo(() => {\n    const base = from || new Date();\n    return addMonths(\n      new Date(base.getFullYear(), base.getMonth(), 1),\n      monthNav,\n    );\n  }, [from, monthNav]);\n\n  const rightView = useMemo(() => addMonths(leftView, 1), [leftView]);\n\n  const presets = useMemo(() => buildPresets(new Date()), []);\n\n  useEffect(() => {\n    function handleClick(e: MouseEvent) {\n      if (\n        wrapperRef.current &&\n        !wrapperRef.current.contains(e.target as Node)\n      ) {\n        setOpen(false);\n      }\n    }\n    document.addEventListener(\"mousedown\", handleClick);\n    return () => document.removeEventListener(\"mousedown\", handleClick);\n  }, []);\n\n  function handleSelect(date: Date) {\n    if (!from || (from && to)) {\n      // start a new selection\n      setDraft({ from: toValue(date), to: \"\" });\n      return;\n    }\n    // completing the range\n    let newFrom = from;\n    let newTo = date;\n    if (date < from) {\n      newFrom = date;\n      newTo = from;\n    }\n    const finalRange = { from: toValue(newFrom), to: toValue(newTo) };\n    setDraft(finalRange);\n    onChange(finalRange);\n    setOpen(false);\n  }\n\n  function handlePreset(preset: Preset) {\n    const range = preset.range();\n    setDraft(range);\n    onChange(range);\n    setOpen(false);\n  }\n\n  const displayLabel =\n    displayFrom && displayTo\n      ? `${formatDisplay(displayFrom)} - ${formatDisplay(displayTo)}`\n      : placeholder;\n\n  function shiftRange(days: number) {\n    if (!displayFrom || !displayTo) return;\n    const range = {\n      from: toValue(addDays(displayFrom, days)),\n      to: toValue(addDays(displayTo, days)),\n    };\n    onChange(range);\n  }\n\n  return (\n    <div\n      className={cn(\n        \"relative inline-flex w-full items-center gap-2 sm:w-auto\",\n        className,\n      )}\n      ref={wrapperRef}\n    >\n      <button\n        type=\"button\"\n        id={id}\n        ref={triggerRef}\n        onClick={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n          setMonthNav(0);\n          setOpen((o) => {\n            if (!o) updatePosition();\n            return !o;\n          });\n        }}\n        className={cn(\n          \"flex h-10 min-w-0 flex-1 items-center gap-2 rounded-md border border-zinc-300 bg-white px-3 text-sm text-zinc-900 sm:min-w-56 sm:flex-none\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-500 focus-visible:ring-offset-2\",\n          \"transition-colors hover:border-zinc-400\",\n        )}\n      >\n        <Calendar className=\"h-4 w-4 shrink-0 text-zinc-500\" />\n        <span className={cn(\"truncate\", !displayFrom && \"text-zinc-500\")}>\n          {displayLabel}\n        </span>\n      </button>\n\n      <button\n        type=\"button\"\n        onClick={() => shiftRange(-1)}\n        className=\"flex h-10 w-9 items-center justify-center rounded-md border border-zinc-300 bg-white text-zinc-600 transition-colors hover:bg-zinc-100\"\n        aria-label=\"Shift range back\"\n      >\n        <ChevronLeft className=\"h-4 w-4\" />\n      </button>\n      <button\n        type=\"button\"\n        onClick={() => shiftRange(1)}\n        className=\"flex h-10 w-9 items-center justify-center rounded-md border border-zinc-300 bg-white text-zinc-600 transition-colors hover:bg-zinc-100\"\n        aria-label=\"Shift range forward\"\n      >\n        <ChevronRight className=\"h-4 w-4\" />\n      </button>\n\n      {open && dropdownPos && (\n        <div\n          ref={dropdownRef}\n          className=\"fixed z-50 flex w-[min(720px,calc(100vw-16px))] overflow-hidden rounded-md border border-zinc-200 bg-white shadow-lg\"\n          style={{ top: dropdownPos.top, left: dropdownPos.left }}\n        >\n          <div className=\"hidden w-40 shrink-0 border-r border-zinc-200 py-2 sm:block\">\n            {presets.map((p) => (\n              <button\n                key={p.label}\n                type=\"button\"\n                onClick={() => handlePreset(p)}\n                className=\"w-full px-4 py-1.5 text-left text-sm text-zinc-900 transition-colors hover:bg-zinc-100\"\n              >\n                {p.label}\n              </button>\n            ))}\n          </div>\n\n          <div className=\"min-w-0 flex-1 p-3\">\n            <div className=\"flex items-center gap-2 md:gap-4\">\n              <button\n                type=\"button\"\n                onClick={() => setMonthNav((m) => m - 1)}\n                className=\"shrink-0 rounded-sm p-1 text-zinc-600 transition-colors hover:bg-zinc-100\"\n                aria-label=\"Previous month\"\n              >\n                <ChevronLeft className=\"h-4 w-4\" />\n              </button>\n\n              <MonthGrid\n                viewDate={leftView}\n                from={from}\n                to={to}\n                hoverDate={hoverDate}\n                minDate={minDate}\n                onSelect={handleSelect}\n                onHover={setHoverDate}\n              />\n              <MonthGrid\n                viewDate={rightView}\n                from={from}\n                to={to}\n                hoverDate={hoverDate}\n                minDate={minDate}\n                onSelect={handleSelect}\n                onHover={setHoverDate}\n                className=\"hidden md:block\"\n              />\n\n              <button\n                type=\"button\"\n                onClick={() => setMonthNav((m) => m + 1)}\n                className=\"shrink-0 rounded-sm p-1 text-zinc-600 transition-colors hover:bg-zinc-100\"\n                aria-label=\"Next month\"\n              >\n                <ChevronRight className=\"h-4 w-4\" />\n              </button>\n            </div>\n\n            {from && (\n              <button\n                type=\"button\"\n                onClick={() => {\n                  onChange({ from: \"\", to: \"\" });\n                  setDraft({ from: \"\", to: \"\" });\n                  setOpen(false);\n                }}\n                className=\"mt-3 w-full border-t border-zinc-200 py-1.5 text-center text-xs text-zinc-500 transition-colors hover:text-zinc-900\"\n              >\n                Clear range\n              </button>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
    }
  ],
  "docs": "Usage:\n\n```tsx\nimport { DateRangePicker } from \"@/components/ui/date-range-picker\";\n\nconst [range, setRange] = useState({ from: \"\", to: \"\" });\n\n<DateRangePicker value={range} onChange={setRange} min=\"2024-01-01\" />\n```\n\nProps: value ({ from, to } as 'YYYY-MM-DD'), onChange, placeholder, min ('YYYY-MM-DD'), id, className."
}
