{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker-bs",
  "type": "registry:ui",
  "title": "Date Range Picker (BS / Nepali)",
  "description": "A Nepali Bikram Sambat date range picker (AD values in, BS calendar UI) with quick presets and a two-month view.",
  "dependencies": [
    "lucide-react",
    "@remotemerge/nepali-date-converter"
  ],
  "files": [
    {
      "path": "components/ui/date-range-picker-bs.tsx",
      "type": "registry:ui",
      "target": "~/components/ui/date-range-picker-bs.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\";\nimport {\n  adToBs,\n  bsToAd,\n  bsDaysInMonth,\n  bsWeekdayOffset,\n  bsMonthName,\n} from \"@/lib/nepali\";\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 startOfWeek(date: Date): Date {\n  const d = new Date(date);\n  const day = d.getDay(); // 0 = Sun ... 6 = Sat\n  return addDays(d, -day);\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\nfunction formatBsDateValue(date: Date): string {\n  const iso = toValue(date);\n  const bs = adToBs(iso);\n  if (!bs) return formatDisplay(date);\n  return `${bs.year} ${bsMonthName(bs.month)} ${bs.date}`;\n}\n\nconst WEEKDAYS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\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  bsYear,\n  bsMonth,\n  from,\n  to,\n  hoverDate,\n  minDate,\n  onSelect,\n  onHover,\n  className,\n}: {\n  bsYear: number;\n  bsMonth: number;\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 daysInMonth = bsDaysInMonth(bsYear, bsMonth);\n  const startOffset = bsWeekdayOffset(bsYear, bsMonth);\n  const cells: (number | null)[] = [\n    ...Array(startOffset).fill(null),\n    ...Array.from({ length: daysInMonth }, (_, i) => i + 1),\n  ];\n  const today = new Date();\n\n  const effectiveEnd = to ?? hoverDate;\n\n  const monthLabel = `${bsMonthName(bsMonth)} ${bsYear}`;\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((day, i) => {\n          if (day === null) return <div key={`empty-${i}`} />;\n          const adIso = bsToAd({ year: bsYear, month: bsMonth, date: day });\n          if (!adIso) return <div key={`empty-${i}`} />;\n          const date = toLocalDate(adIso)!;\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                isDisabled && \"cursor-not-allowed text-zinc-300\",\n                !isDisabled &&\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-emerald-600 font-semibold text-white hover:bg-emerald-600\",\n              )}\n            >\n              {day}\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 [view, setView] = useState<{ year: number; month: number }>(() => {\n    const bs = adToBs(value.from || new Date().toISOString().slice(0, 10));\n    return { year: bs?.year ?? 2080, month: bs?.month ?? 1 };\n  });\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 = view;\n  const rightView = {\n    year: view.month === 12 ? view.year + 1 : view.year,\n    month: view.month === 12 ? 1 : view.month + 1,\n  };\n\n  const shiftView = useCallback((dir: -1 | 1) => {\n    setView((v) => {\n      if (dir === -1) {\n        return v.month === 1\n          ? { year: v.year - 1, month: 12 }\n          : { year: v.year, month: v.month - 1 };\n      }\n      return v.month === 12\n        ? { year: v.year + 1, month: 1 }\n        : { year: v.year, month: v.month + 1 };\n    });\n  }, []);\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      setDraft({ from: toValue(date), to: \"\" });\n      return;\n    }\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      ? `${formatBsDateValue(displayFrom)} - ${formatBsDateValue(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          const bs = adToBs(\n            value.from || new Date().toISOString().slice(0, 10),\n          );\n          if (bs) setView({ year: bs.year, month: bs.month });\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={() => shiftView(-1)}\n                className=\"shrink-0 rounded-sm p-1 text-zinc-600 hover:bg-zinc-100\"\n                aria-label=\"Previous month\"\n              >\n                <ChevronLeft className=\"h-4 w-4\" />\n              </button>\n\n              <MonthGrid\n                bsYear={leftView.year}\n                bsMonth={leftView.month}\n                from={from}\n                to={to}\n                hoverDate={hoverDate}\n                minDate={minDate}\n                onSelect={handleSelect}\n                onHover={setHoverDate}\n              />\n              <MonthGrid\n                bsYear={rightView.year}\n                bsMonth={rightView.month}\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={() => shiftView(1)}\n                className=\"shrink-0 rounded-sm p-1 text-zinc-600 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 hover:text-zinc-900\"\n              >\n                Clear range\n              </button>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "path": "lib/nepali.ts",
      "type": "registry:ui",
      "target": "~/lib/nepali.ts",
      "content": "import DateConverter from \"@remotemerge/nepali-date-converter\";\n\nconst BS_MONTHS = [\n  \"Baishakh\",\n  \"Jestha\",\n  \"Ashadh\",\n  \"Shrawan\",\n  \"Bhadra\",\n  \"Ashwin\",\n  \"Kartik\",\n  \"Mangsir\",\n  \"Poush\",\n  \"Magh\",\n  \"Falgun\",\n  \"Chaitra\",\n] as const;\n\nexport type BsDate = { year: number; month: number; date: number };\n\nexport function adToBs(isoDate: string): BsDate | null {\n  try {\n    const bs = new DateConverter(isoDate).toBs();\n    return { year: bs.year, month: bs.month, date: bs.date };\n  } catch {\n    return null;\n  }\n}\n\nexport function bsToAd(bs: BsDate): string | null {\n  try {\n    const ad = new DateConverter(\n      `${bs.year}-${String(bs.month).padStart(2, \"0\")}-${String(\n        bs.date,\n      ).padStart(2, \"0\")}`,\n    ).toAd();\n    return `${ad.year}-${String(ad.month).padStart(2, \"0\")}-${String(\n      ad.date,\n    ).padStart(2, \"0\")}`;\n  } catch {\n    return null;\n  }\n}\n\nexport function bsDaysInMonth(year: number, month: number): number {\n  const start = bsToAd({ year, month, date: 1 });\n  const next =\n    month === 12\n      ? { year: year + 1, month: 1, date: 1 }\n      : { year, month: month + 1, date: 1 };\n  const nextAd = bsToAd(next);\n  if (!start || !nextAd) return 30;\n  const diff =\n    (new Date(`${nextAd}T00:00:00Z`).getTime() -\n      new Date(`${start}T00:00:00Z`).getTime()) /\n    86400000;\n  return Math.round(diff);\n}\n\nexport function bsWeekdayOffset(year: number, month: number): number {\n  const start = bsToAd({ year, month, date: 1 });\n  if (!start) return 0;\n  return new Date(`${start}T00:00:00Z`).getUTCDay();\n}\n\nexport function bsMonthName(month: number): string {\n  return BS_MONTHS[(month - 1 + 12) % 12] ?? \"\";\n}\n\nexport function formatBsDate(isoDate: string): string {\n  const bs = adToBs(isoDate);\n  if (!bs) return isoDate;\n  return `${bs.year} ${bsMonthName(bs.month)} ${bs.date}`;\n}\n"
    }
  ],
  "docs": "Usage:\n\n```tsx\nimport { DateRangePicker } from \"@/components/ui/date-range-picker-bs\";\n\nconst [range, setRange] = useState({ from: \"\", to: \"\" });\n\n<DateRangePicker value={range} onChange={setRange} />\n```\n\nValues stay AD 'YYYY-MM-DD' strings; the calendar renders in Bikram Sambat.\nAlso installs lib/nepali.ts automatically."
}
