{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "range-calendar",
  "type": "registry:component",
  "title": "Range Calendar",
  "description": "Pick a range from two months side by side or a one-click preset, watch it preview as you hover, and line it up against the period before with a single tap.",
  "categories": [
    "component",
    "Forms"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/calendar.json",
    "https://coss.com/ui/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/devl/range-calendar.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ArrowRightIcon, CalendarRangeIcon, CheckIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type KeyboardEvent, useId, useMemo, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DateRange {\n  from: Date;\n  to: Date;\n}\n\ntype Draft = { from?: Date; to?: Date };\n\nconst DAY = 86_400_000;\nconst startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());\nconst addDays = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nconst addMonths = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth() + n, 1);\nconst startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);\nconst endOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth() + 1, 0);\nconst sameDay = (a?: Date, b?: Date) => !!a && !!b && a.getTime() === b.getTime();\nconst daysBetween = (a: Date, b: Date) => Math.round((startOfDay(b).getTime() - startOfDay(a).getTime()) / DAY) + 1;\n\nexport interface DatePreset {\n  label: string;\n  range: (today: Date) => DateRange;\n}\n\nexport const defaultPresets: DatePreset[] = [\n  { label: \"Today\", range: (t) => ({ from: t, to: t }) },\n  { label: \"Yesterday\", range: (t) => ({ from: addDays(t, -1), to: addDays(t, -1) }) },\n  { label: \"Last 7 days\", range: (t) => ({ from: addDays(t, -6), to: t }) },\n  { label: \"Last 30 days\", range: (t) => ({ from: addDays(t, -29), to: t }) },\n  { label: \"This month\", range: (t) => ({ from: startOfMonth(t), to: t }) },\n  { label: \"Last month\", range: (t) => ({ from: addMonths(t, -1), to: endOfMonth(addMonths(t, -1)) }) },\n  {\n    label: \"Quarter to date\",\n    range: (t) => ({ from: new Date(t.getFullYear(), Math.floor(t.getMonth() / 3) * 3, 1), to: t }),\n  },\n  { label: \"Year to date\", range: (t) => ({ from: new Date(t.getFullYear(), 0, 1), to: t }) },\n];\n\nexport function previousPeriod(range: DateRange): DateRange {\n  const length = daysBetween(range.from, range.to);\n  return { from: addDays(range.from, -length), to: addDays(range.from, -1) };\n}\n\nconst fmt = new Intl.DateTimeFormat(\"en-US\", { month: \"short\", day: \"numeric\" });\nconst fmtYear = new Intl.DateTimeFormat(\"en-US\", { month: \"short\", day: \"numeric\", year: \"numeric\" });\nfunction formatRange(r: Draft) {\n  if (!r.from) return \"Pick a start date\";\n  if (!r.to) return `${fmt.format(r.from)} – …`;\n  if (sameDay(r.from, r.to)) return fmtYear.format(r.from);\n  const sameYear = r.from.getFullYear() === r.to.getFullYear();\n  return `${(sameYear ? fmt : fmtYear).format(r.from)} – ${fmtYear.format(r.to)}`;\n}\n\nexport interface RangeCalendarProps {\n  defaultValue?: DateRange;\n  presets?: DatePreset[];\n  disableFuture?: boolean;\n  defaultCompare?: boolean;\n  onApply?: (range: DateRange, compareTo: DateRange | null) => void;\n  onCancel?: () => void;\n  today?: Date;\n  className?: string;\n}\n\nexport function RangeCalendar({\n  defaultValue,\n  presets = defaultPresets,\n  disableFuture = true,\n  defaultCompare = false,\n  onApply,\n  onCancel,\n  today: todayProp,\n  className,\n}: RangeCalendarProps) {\n  const today = useMemo(() => startOfDay(todayProp ?? new Date()), [todayProp]);\n  const initial = defaultValue ?? presets[2]?.range(today) ?? { from: addDays(today, -6), to: today };\n  const [range, setRange] = useState<Draft>(initial);\n  const [hover, setHover] = useState<Date>();\n  const [month, setMonth] = useState(() => addMonths(initial.to, -1));\n  const [compare, setCompare] = useState(defaultCompare);\n  const [applied, setApplied] = useState(false);\n  const reduce = useReducedMotion();\n  const compareId = useId();\n\n  const activePreset = presets.findIndex((p) => {\n    const r = p.range(today);\n    return sameDay(r.from, range.from) && sameDay(r.to, range.to);\n  });\n\n  const choosePreset = (index: number) => {\n    const r = presets[index].range(today);\n    setRange(r);\n    setHover(undefined);\n    setApplied(false);\n    setMonth(addMonths(r.to, -1));\n  };\n\n  const pick = (day: Date) => {\n    setApplied(false);\n    setRange((r) => {\n      if (!r.from || r.to) return { from: day, to: undefined };\n      return day < r.from ? { from: day, to: r.from } : { from: r.from, to: day };\n    });\n  };\n\n  const preview = range.from && !range.to && hover ? (hover < range.from ? { from: hover, to: range.from } : { from: range.from, to: hover }) : undefined;\n  const complete = range.from && range.to ? (range as DateRange) : undefined;\n  const comparison = compare && complete ? previousPeriod(complete) : undefined;\n  const shown = preview ?? range;\n  const length = shown.from && shown.to ? daysBetween(shown.from, shown.to) : shown.from ? 1 : 0;\n\n  const onPresetKey = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n    const next = event.key === \"ArrowDown\" ? index + 1 : event.key === \"ArrowUp\" ? index - 1 : null;\n    if (next === null) return;\n    event.preventDefault();\n    const target = (next + presets.length) % presets.length;\n    choosePreset(target);\n    event.currentTarget.parentElement?.querySelectorAll<HTMLButtonElement>(\"button\")[target]?.focus();\n  };\n\n  return (\n    <div className={cn(\"w-full max-w-160 overflow-hidden rounded-2xl border bg-card shadow-xs/5\", className)}>\n      <div className=\"flex flex-col sm:flex-row\">\n        <div\n          aria-label=\"Presets\"\n          className=\"relative flex gap-1 overflow-x-auto border-b p-2 sm:w-40 sm:shrink-0 sm:flex-col sm:overflow-visible sm:border-e sm:border-b-0\"\n          role=\"listbox\"\n        >\n          {presets.map((preset, i) => {\n            const active = i === activePreset;\n            return (\n              <button\n                aria-selected={active}\n                className={cn(\n                  \"relative shrink-0 cursor-pointer whitespace-nowrap rounded-lg px-2.5 py-1.5 text-start text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n                  active ? \"font-medium text-foreground\" : \"text-muted-foreground hover:text-foreground\",\n                )}\n                key={preset.label}\n                onClick={() => choosePreset(i)}\n                onKeyDown={(e) => onPresetKey(e, i)}\n                role=\"option\"\n                tabIndex={active || (activePreset === -1 && i === 0) ? 0 : -1}\n                type=\"button\"\n              >\n                {active && (\n                  <motion.span\n                    className=\"absolute inset-0 -z-0 rounded-lg bg-accent\"\n                    layoutId={`${compareId}-preset`}\n                    transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 500, damping: 36 }}\n                  />\n                )}\n                <span className=\"relative\">{preset.label}</span>\n              </button>\n            );\n          })}\n        </div>\n\n        <div className=\"min-w-0 flex-1 p-3\">\n          <Calendar\n            className=\"mx-auto [--cell-size:--spacing(8)] sm:[--cell-size:--spacing(8)]\"\n            classNames={{ day_button: \"in-data-outside:text-muted-foreground\", weekday: \"text-muted-foreground\" }}\n            disabled={disableFuture ? { after: today } : undefined}\n            mode=\"range\"\n            modifiers={{\n              preview: preview ? { from: preview.from, to: preview.to } : [],\n              previewEnd: preview ? [preview.from, preview.to].filter((d) => !sameDay(d, range.from)) : [],\n              compare: comparison ? { from: comparison.from, to: comparison.to } : [],\n              compareStart: comparison ? comparison.from : [],\n              compareEnd: comparison ? comparison.to : [],\n            }}\n            modifiersClassNames={{\n              preview:\n                \"[&:not([data-selected]):not([data-outside])>button]:rounded-none [&:not([data-selected]):not([data-outside])>button]:bg-accent\",\n              previewEnd:\n                \"[&>button]:rounded-lg! [&>button]:bg-transparent! [&>button]:ring-2 [&>button]:ring-ring [&>button]:ring-inset\",\n              compare: \"[&:not([data-selected]):not([data-outside])>button]:rounded-none [&:not([data-selected]):not([data-outside])>button]:bg-info/16\",\n              compareStart: \"[&:not([data-selected]):not([data-outside])>button]:rounded-s-lg!\",\n              compareEnd: \"[&:not([data-selected]):not([data-outside])>button]:rounded-e-lg!\",\n            }}\n            month={month}\n            numberOfMonths={2}\n            onDayMouseEnter={(day) => setHover(day)}\n            onDayMouseLeave={() => setHover(undefined)}\n            onDayFocus={(day: Date) => setHover(day)}\n            onMonthChange={setMonth}\n            onSelect={(_: unknown, day: Date) => pick(day)}\n            selected={range.from ? { from: range.from, to: range.to } : undefined}\n          />\n        </div>\n      </div>\n\n      <div className=\"grid gap-3 border-t p-3 sm:flex sm:items-center sm:justify-between\">\n        <div className=\"grid min-w-0 gap-1\">\n          <div className=\"flex items-center gap-2 text-sm\">\n            <CalendarRangeIcon className=\"size-4 shrink-0 text-muted-foreground\" />\n            <span className={cn(\"truncate font-medium tabular-nums transition-colors\", preview && \"text-muted-foreground\")}>\n              {formatRange(shown)}\n            </span>\n            {length > 0 && (\n              <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-px font-mono text-[11px] text-muted-foreground tabular-nums\">\n                {length}d\n              </span>\n            )}\n          </div>\n          <label className=\"flex items-center gap-2 text-muted-foreground text-xs\" htmlFor={compareId}>\n            <Switch checked={compare} id={compareId} onCheckedChange={setCompare} />\n            <span>Compare to previous period</span>\n            <AnimatePresence initial={false}>\n              {comparison && (\n                <motion.span\n                  animate={{ opacity: 1, x: 0 }}\n                  className=\"flex items-center gap-1 whitespace-nowrap font-medium text-foreground tabular-nums\"\n                  exit={{ opacity: 0, x: -6, transition: { duration: 0.12 } }}\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, x: -6 }}\n                  transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}\n                >\n                  <span aria-hidden className=\"size-2 rounded-full bg-info\" />\n                  {formatRange(comparison)}\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </label>\n        </div>\n        <div className=\"flex shrink-0 justify-end gap-2\">\n          <Button onClick={onCancel} size=\"sm\" variant=\"ghost\">\n            Cancel\n          </Button>\n          <Button\n            disabled={!complete}\n            onClick={() => {\n              if (!complete) return;\n              setApplied(true);\n              onApply?.(complete, comparison ?? null);\n            }}\n            className=\"min-w-24\"\n            size=\"sm\"\n          >\n            {applied ? <CheckIcon /> : null}\n            {applied ? \"Applied\" : \"Apply\"}\n            {!applied && <ArrowRightIcon />}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}