{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "title": "Date Range Picker",
  "author": "Zubeyr <https://zubeyr.dev>",
  "description": "An analytics-style range picker: presets, typed start and end dates, two months side by side, hover preview, keyboard navigation and Apply/Cancel.",
  "files": [
    {
      "path": "registry/dashboard/components/date-range-picker/date-range-picker.tsx",
      "content": "'use client'\n\nimport { useEffect, useRef, useState } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nimport { CalendarMonth, monthTitle } from './calendar-month'\nimport {\n  addDays,\n  defaultPresets,\n  formatRange,\n  fromInputValue,\n  sameMonth,\n  sameRange,\n  startOfDay,\n  startOfMonth,\n  toInputValue,\n  type DatePreset,\n  type DateRange,\n} from './dates'\n\nexport type { DatePreset, DateRange }\nexport { addDays, defaultPresets, startOfDay }\n\ntype DateRangePickerProps = {\n  value?: DateRange\n  defaultValue?: DateRange\n  onChange?: (range: DateRange, preset?: string) => void\n  presets?: DatePreset[]\n  maxDate?: Date\n  disableFuture?: boolean\n  align?: 'start' | 'end'\n  className?: string\n}\n\nconst keyOffsets: Record<string, number> = {\n  ArrowLeft: -1,\n  ArrowRight: 1,\n  ArrowUp: -7,\n  ArrowDown: 7,\n}\n\nexport function DateRangePicker({\n  value,\n  defaultValue,\n  onChange,\n  presets = defaultPresets,\n  maxDate: maxDateProp,\n  disableFuture = false,\n  align = 'start',\n  className,\n}: DateRangePickerProps) {\n  const [today] = useState(() => startOfDay(new Date()))\n  const maxDate = disableFuture ? today : maxDateProp\n  const [internal, setInternal] = useState<DateRange>(\n    () => defaultValue ?? presets[3]?.range(today) ?? { start: today, end: today },\n  )\n  const committed = value ?? internal\n\n  const [open, setOpen] = useState(false)\n  const [start, setStart] = useState<Date | null>(committed.start)\n  const [end, setEnd] = useState<Date | null>(committed.end)\n  const [preview, setPreview] = useState<Date | null>(null)\n  const [view, setView] = useState(() => startOfMonth(committed.end, -1))\n  const [focused, setFocused] = useState(committed.end)\n  const [inputs, setInputs] = useState({ start: '', end: '' })\n\n  const root = useRef<HTMLDivElement>(null)\n  const trigger = useRef<HTMLButtonElement>(null)\n  const grid = useRef<HTMLDivElement>(null)\n  const keyboard = useRef(false)\n\n  const presetFor = (range: DateRange) => presets.find((p) => sameRange(p.range(today), range))\n  const draft = start && end ? { start, end } : null\n  const activePreset = draft ? presetFor(draft)?.label : undefined\n  const committedPreset = presetFor(committed)?.label\n\n  const show = (range: { start: Date | null; end: Date | null }) => {\n    setStart(range.start)\n    setEnd(range.end)\n    setInputs({\n      start: range.start ? toInputValue(range.start) : '',\n      end: range.end ? toInputValue(range.end) : '',\n    })\n  }\n\n  const reveal = (date: Date) => {\n    if (!sameMonth(date, view) && !sameMonth(date, startOfMonth(view, 1)))\n      setView(startOfMonth(date, -1))\n  }\n\n  const openPicker = () => {\n    show(committed)\n    setView(startOfMonth(committed.end, -1))\n    setFocused(committed.end)\n    setOpen(true)\n  }\n\n  const close = () => {\n    setOpen(false)\n    setPreview(null)\n    trigger.current?.focus()\n  }\n\n  const apply = () => {\n    if (!start) return\n    const range = { start, end: end ?? start }\n    if (value === undefined) setInternal(range)\n    onChange?.(range, presetFor(range)?.label)\n    close()\n  }\n\n  const select = (date: Date) => {\n    setFocused(date)\n    if (!start || end) return show({ start: date, end: null })\n    show(date < start ? { start: date, end: start } : { start, end: date })\n  }\n\n  const choosePreset = (preset: DatePreset) => {\n    const range = preset.range(today)\n    show(range)\n    setView(startOfMonth(range.end, -1))\n    setFocused(range.end)\n  }\n\n  const commitInput = (key: 'start' | 'end') => {\n    const date = fromInputValue(inputs[key])\n    if (!date || (maxDate && date > maxDate)) return show({ start, end })\n    const next =\n      key === 'start' ? { start: date, end: end ?? date } : { start: start ?? date, end: date }\n    show(next.start <= next.end ? next : { start: next.end, end: next.start })\n    reveal(date)\n    setFocused(date)\n  }\n\n  const onDayKeyDown = (event: React.KeyboardEvent, date: Date) => {\n    const offset = keyOffsets[event.key]\n    if (offset === undefined) return\n    event.preventDefault()\n    const next = addDays(date, offset)\n    keyboard.current = true\n    setFocused(next)\n    reveal(next)\n    if (start && !end) setPreview(next)\n  }\n\n  useEffect(() => {\n    if (!keyboard.current) return\n    keyboard.current = false\n    grid.current?.querySelector<HTMLButtonElement>(`[data-date=\"${focused.getTime()}\"]`)?.focus()\n  }, [focused, view])\n\n  useEffect(() => {\n    if (!open) return\n    const onPointer = (event: PointerEvent) => {\n      if (!root.current?.contains(event.target as Node)) setOpen(false)\n    }\n    const onKey = (event: KeyboardEvent) => event.key === 'Escape' && close()\n    document.addEventListener('pointerdown', onPointer)\n    document.addEventListener('keydown', onKey)\n    return () => {\n      document.removeEventListener('pointerdown', onPointer)\n      document.removeEventListener('keydown', onKey)\n    }\n  })\n\n  const month = (offset: number, header: React.ReactNode) => (\n    <CalendarMonth\n      month={startOfMonth(view, offset)}\n      start={start}\n      end={end}\n      preview={start && !end ? preview : null}\n      focused={focused}\n      today={today}\n      maxDate={maxDate}\n      onSelect={select}\n      onHover={setPreview}\n      onKeyDown={onDayKeyDown}\n      header={header}\n    />\n  )\n\n  const nav = (direction: -1 | 1, label: string) => (\n    <button\n      type=\"button\"\n      aria-label={label}\n      onClick={() => setView(startOfMonth(view, direction))}\n      className=\"flex size-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground\"\n    >\n      <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-4\" aria-hidden>\n        <path\n          d={direction < 0 ? 'M10 3 5 8l5 5' : 'm6 3 5 5-5 5'}\n          stroke=\"currentColor\"\n          strokeWidth=\"1.6\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </button>\n  )\n\n  const title = (offset: number) => (\n    <span className=\"flex-1 text-center text-sm font-semibold\">\n      {monthTitle(startOfMonth(view, offset))}\n    </span>\n  )\n\n  return (\n    <div ref={root} className={cn('relative inline-flex', className)}>\n      <button\n        ref={trigger}\n        type=\"button\"\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        onClick={() => (open ? close() : openPicker())}\n        className=\"inline-flex h-8 items-center gap-1.5 rounded-lg bg-card px-3 text-sm font-medium text-foreground shadow-(--shadow-button) transition-colors outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60\"\n      >\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3.5\" aria-hidden>\n          <rect x=\"2\" y=\"3\" width=\"12\" height=\"11\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"1.4\" />\n          <path\n            d=\"M2 6.5h12M5.5 1.5v3M10.5 1.5v3\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.4\"\n            strokeLinecap=\"round\"\n          />\n        </svg>\n        {committedPreset ?? formatRange(committed)}\n      </button>\n\n      {open && (\n        <div\n          role=\"dialog\"\n          aria-label=\"Choose a date range\"\n          className={cn(\n            'absolute top-full z-50 mt-2 flex max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl bg-popover text-popover-foreground shadow-(--shadow-overlay)',\n            align === 'end' ? 'right-0' : 'left-0',\n          )}\n        >\n          <ul className=\"hidden w-44 shrink-0 flex-col gap-px overflow-y-auto border-r p-2 md:flex\">\n            {presets.map((preset) => (\n              <li key={preset.label}>\n                <button\n                  type=\"button\"\n                  onClick={() => choosePreset(preset)}\n                  aria-pressed={activePreset === preset.label}\n                  className={cn(\n                    'flex h-8 w-full items-center rounded-lg px-2.5 text-left text-sm hover:bg-muted',\n                    activePreset === preset.label && 'bg-muted font-medium',\n                  )}\n                >\n                  {preset.label}\n                </button>\n              </li>\n            ))}\n          </ul>\n\n          <div className=\"flex flex-col gap-4 p-4\">\n            <div className=\"flex flex-wrap gap-1.5 md:hidden\">\n              {presets.map((preset) => (\n                <button\n                  key={preset.label}\n                  type=\"button\"\n                  aria-pressed={activePreset === preset.label}\n                  onClick={() => choosePreset(preset)}\n                  className={cn(\n                    'h-7 rounded-full border px-3 text-xs font-medium',\n                    activePreset === preset.label\n                      ? 'border-transparent bg-primary text-primary-foreground'\n                      : 'hover:bg-muted',\n                  )}\n                >\n                  {preset.label}\n                </button>\n              ))}\n            </div>\n\n            <div className=\"flex items-center gap-2\">\n              {(['start', 'end'] as const).map((key, i) => (\n                <div key={key} className=\"contents\">\n                  {i === 1 && <span className=\"text-muted-foreground\">→</span>}\n                  <input\n                    aria-label={key === 'start' ? 'Start date' : 'End date'}\n                    placeholder=\"YYYY-MM-DD\"\n                    value={inputs[key]}\n                    onChange={(e) => setInputs({ ...inputs, [key]: e.target.value })}\n                    onBlur={() => commitInput(key)}\n                    onKeyDown={(e) => e.key === 'Enter' && commitInput(key)}\n                    className=\"h-8 w-full min-w-0 rounded-lg border border-input bg-card px-2.5 text-sm tabular-nums outline-none focus:border-ring\"\n                  />\n                </div>\n              ))}\n            </div>\n\n            <div ref={grid} className=\"flex gap-6\">\n              {month(\n                0,\n                <>\n                  {nav(-1, 'Previous month')}\n                  {title(0)}\n                  <span className=\"size-8 max-md:hidden\" />\n                  <span className=\"md:hidden\">{nav(1, 'Next month')}</span>\n                </>,\n              )}\n              <div className=\"max-md:hidden\">\n                {month(\n                  1,\n                  <>\n                    <span className=\"size-8\" />\n                    {title(1)}\n                    {nav(1, 'Next month')}\n                  </>,\n                )}\n              </div>\n            </div>\n\n            <div className=\"flex items-center justify-between gap-2 border-t pt-4\">\n              <span className=\"text-xs text-muted-foreground\">\n                {draft ? formatRange(draft) : 'Select an end date'}\n              </span>\n              <div className=\"flex gap-2\">\n                <button\n                  type=\"button\"\n                  onClick={close}\n                  className=\"h-8 rounded-lg bg-card px-3 text-sm font-medium shadow-(--shadow-button) hover:bg-muted\"\n                >\n                  Cancel\n                </button>\n                <button\n                  type=\"button\"\n                  onClick={apply}\n                  disabled={!draft}\n                  className=\"h-8 rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50\"\n                >\n                  Apply\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/dashboard/date-range-picker/date-range-picker.tsx"
    },
    {
      "path": "registry/dashboard/components/date-range-picker/calendar-month.tsx",
      "content": "import { cn } from '@/lib/utils'\n\nimport { addDays, sameDay } from './dates'\n\nconst formats = new Map<\n  string,\n  { weekdays: string[]; month: Intl.DateTimeFormat; label: Intl.DateTimeFormat }\n>()\nfunction formatsFor(locale = 'en-US') {\n  let f = formats.get(locale)\n  if (!f) {\n    const day = new Intl.DateTimeFormat(locale, { weekday: 'short' })\n    f = {\n      weekdays: Array.from({ length: 7 }, (_, i) =>\n        day.format(new Date(2024, 0, 7 + i)).slice(0, 2),\n      ),\n      month: new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }),\n      label: new Intl.DateTimeFormat(locale, { dateStyle: 'full' }),\n    }\n    formats.set(locale, f)\n  }\n  return f\n}\n\ntype CalendarMonthProps = {\n  month: Date\n  start: Date | null\n  end: Date | null\n  preview: Date | null\n  focused: Date\n  today: Date\n  minDate?: Date\n  maxDate?: Date\n  onSelect: (date: Date) => void\n  onHover: (date: Date | null) => void\n  onKeyDown: (event: React.KeyboardEvent, date: Date) => void\n  header?: React.ReactNode\n  locale?: string\n}\n\nexport function CalendarMonth({\n  month,\n  start,\n  end,\n  preview,\n  focused,\n  today,\n  minDate,\n  maxDate,\n  onSelect,\n  onHover,\n  onKeyDown,\n  header,\n  locale,\n}: CalendarMonthProps) {\n  const { weekdays, month: monthFormat, label: labelFormat } = formatsFor(locale)\n  const first = new Date(month.getFullYear(), month.getMonth(), 1)\n  const length = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate()\n  const days = Array.from({ length }, (_, i) => addDays(first, i))\n  const rangeEnd = end ?? preview\n  const [from, to] =\n    start && rangeEnd ? (start <= rangeEnd ? [start, rangeEnd] : [rangeEnd, start]) : [start, start]\n\n  return (\n    <div className=\"flex w-[15.75rem] flex-col gap-3\">\n      <div className=\"flex h-8 items-center justify-between\">{header}</div>\n      <p className=\"sr-only\">{monthFormat.format(month)}</p>\n      <div role=\"grid\" className=\"grid grid-cols-7 gap-y-1\" onMouseLeave={() => onHover(null)}>\n        {weekdays.map((d) => (\n          <span key={d} className=\"py-1 text-center text-xs text-muted-foreground\">\n            {d}\n          </span>\n        ))}\n        {Array.from({ length: first.getDay() }, (_, i) => (\n          <span key={`blank-${i}`} />\n        ))}\n        {days.map((day) => {\n          const disabled = (!!maxDate && day > maxDate) || (!!minDate && day < minDate)\n          const isStart = sameDay(day, from)\n          const isEnd = sameDay(day, to)\n          const inRange = !!from && !!to && day >= from && day <= to && !sameDay(from, to)\n\n          return (\n            <div\n              key={day.getDate()}\n              className={cn(\n                inRange && 'bg-muted',\n                inRange && isStart && 'rounded-l-full',\n                inRange && isEnd && 'rounded-r-full',\n              )}\n            >\n              <button\n                type=\"button\"\n                data-date={day.getTime()}\n                tabIndex={sameDay(day, focused) ? 0 : -1}\n                disabled={disabled}\n                aria-label={labelFormat.format(day)}\n                aria-pressed={isStart || isEnd}\n                onClick={() => onSelect(day)}\n                onMouseEnter={() => onHover(day)}\n                onKeyDown={(event) => onKeyDown(event, day)}\n                className={cn(\n                  'mx-auto flex size-9 items-center justify-center rounded-full text-sm tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:cursor-not-allowed disabled:opacity-30',\n                  isStart || isEnd\n                    ? 'bg-primary text-primary-foreground'\n                    : 'hover:bg-muted hover:ring-1 hover:ring-border',\n                  sameDay(day, today) && 'font-bold',\n                )}\n              >\n                {day.getDate()}\n              </button>\n            </div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n\nexport const monthTitle = (month: Date, locale?: string) => formatsFor(locale).month.format(month)\n",
      "type": "registry:component",
      "target": "components/dashboard/date-range-picker/calendar-month.tsx"
    },
    {
      "path": "registry/dashboard/components/date-range-picker/dates.ts",
      "content": "export type DateRange = { start: Date; end: Date }\nexport type DatePreset = { label: string; range: (today: Date) => DateRange }\n\nexport const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate())\nexport const addDays = (d: Date, n: number) =>\n  new Date(d.getFullYear(), d.getMonth(), d.getDate() + n)\nexport const startOfMonth = (d: Date, offset = 0) =>\n  new Date(d.getFullYear(), d.getMonth() + offset, 1)\nexport const sameDay = (a?: Date | null, b?: Date | null) =>\n  !!a && !!b && a.getTime() === b.getTime()\nexport const sameMonth = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth()\nexport const sameRange = (a: DateRange, b: DateRange) =>\n  sameDay(a.start, b.start) && sameDay(a.end, b.end)\n\nconst pad = (n: number) => String(n).padStart(2, '0')\n\nexport const toInputValue = (d: Date) =>\n  `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n\nexport function fromInputValue(value: string) {\n  const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value.trim())\n  if (!match) return null\n  const [y, m, d] = match.slice(1).map(Number)\n  const date = new Date(y, m - 1, d)\n  return date.getMonth() === m - 1 ? date : null\n}\n\nconst dayFormat = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' })\nconst fullFormat = new Intl.DateTimeFormat('en-US', {\n  month: 'short',\n  day: 'numeric',\n  year: 'numeric',\n})\n\nexport function formatRange({ start, end }: DateRange) {\n  if (sameDay(start, end)) return fullFormat.format(start)\n  if (sameMonth(start, end))\n    return `${dayFormat.format(start)}–${fullFormat.format(end).replace(/^\\w+ /, '')}`\n  return start.getFullYear() === end.getFullYear()\n    ? `${dayFormat.format(start)}–${fullFormat.format(end)}`\n    : `${fullFormat.format(start)}–${fullFormat.format(end)}`\n}\n\nexport const defaultPresets: DatePreset[] = [\n  { label: 'Today', range: (t) => ({ start: t, end: t }) },\n  { label: 'Yesterday', range: (t) => ({ start: addDays(t, -1), end: addDays(t, -1) }) },\n  { label: 'Last 7 days', range: (t) => ({ start: addDays(t, -6), end: t }) },\n  { label: 'Last 30 days', range: (t) => ({ start: addDays(t, -29), end: t }) },\n  { label: 'Last 90 days', range: (t) => ({ start: addDays(t, -89), end: t }) },\n  { label: 'Last 365 days', range: (t) => ({ start: addDays(t, -364), end: t }) },\n  {\n    label: 'Last month',\n    range: (t) => ({ start: startOfMonth(t, -1), end: addDays(startOfMonth(t), -1) }),\n  },\n  {\n    label: 'Last 12 months',\n    range: (t) => ({ start: startOfMonth(t, -12), end: addDays(startOfMonth(t), -1) }),\n  },\n  {\n    label: 'Last year',\n    range: (t) => ({\n      start: new Date(t.getFullYear() - 1, 0, 1),\n      end: new Date(t.getFullYear() - 1, 11, 31),\n    }),\n  },\n  { label: 'Week to date', range: (t) => ({ start: addDays(t, -t.getDay()), end: t }) },\n  { label: 'Month to date', range: (t) => ({ start: startOfMonth(t), end: t }) },\n  {\n    label: 'Quarter to date',\n    range: (t) => ({\n      start: new Date(t.getFullYear(), t.getMonth() - (t.getMonth() % 3), 1),\n      end: t,\n    }),\n  },\n  { label: 'Year to date', range: (t) => ({ start: new Date(t.getFullYear(), 0, 1), end: t }) },\n]\n",
      "type": "registry:component",
      "target": "components/dashboard/date-range-picker/dates.ts"
    }
  ],
  "cssVars": {
    "light": {
      "shadow-button": "0 1px 2px rgb(0 0 0 / 0.05), 0 0 0 1px rgb(0 0 0 / 0.1), inset 0 1px 0 rgb(255 255 255 / 0.8)",
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.12), 0 0 0 1px rgb(0 0 0 / 0.08)"
    },
    "dark": {
      "shadow-button": "0 0 0 1px rgb(0 0 0 / 0.4), inset 0 1px 0 rgb(255 255 255 / 0.1), inset 0 0 0 1px rgb(255 255 255 / 0.06)",
      "shadow-overlay": "0 16px 40px rgb(0 0 0 / 0.5), 0 0 0 1px #0e0e0e"
    }
  },
  "meta": {
    "scenario": "dashboard",
    "kind": "components"
  },
  "categories": [
    "dashboard",
    "inputs",
    "date"
  ],
  "type": "registry:component"
}