{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "type": "registry:ui",
  "title": "Date Range Picker",
  "description": "Two months, presets that sweep the range in, and a draft until Apply.",
  "categories": [
    "dates"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/calendar.json"
  ],
  "files": [
    {
      "path": "components/ui/date-range-picker.tsx",
      "content": "\"use client\";\nimport { Popover } from \"@base-ui/react/popover\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup } from \"@base-ui/react/toggle-group\";\nimport NumberFlow from \"@number-flow/react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId, useMemo, useState, useSyncExternalStore } from \"react\";\nimport {\n  Calendar,\n  addDays,\n  addMonths,\n  compareDays,\n  diffDays,\n  isSameDay,\n  startOfMonth,\n  toISODate,\n  useResolvedLocale,\n  useToday,\n  type DateRange,\n} from \"@/components/ui/calendar\";\nimport { cn } from \"@/lib/cn\";\nimport { Calendar as CalendarIcon, ChevronDown } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\nexport type CompleteRange = { start: Date; end: Date };\nexport type DateRangePreset = { label: string; range: (today: Date) => CompleteRange };\n\n/** The shortcuts analytics and billing screens reach for. Pass your own to replace them. */\nexport const defaultPresets: DateRangePreset[] = [\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: \"This month\", range: (t) => ({ start: startOfMonth(t), end: t }) },\n  { label: \"Last month\", range: (t) => ({ start: addMonths(startOfMonth(t), -1), end: addDays(startOfMonth(t), -1) }) },\n  { label: \"Last 90 days\", range: (t) => ({ start: addDays(t, -89), end: t }) },\n  { label: \"Year to date\", range: (t) => ({ start: new Date(t.getFullYear(), 0, 1), end: t }) },\n];\n\nconst sameRange = (a: DateRange | null | undefined, b: DateRange | null | undefined) =>\n  !!a && !!b && isSameDay(a.start, b.start) && (a.end === b.end || isSameDay(a.end, b.end));\n\nfunction subscribeWide(notify: () => void) {\n  const mq = window.matchMedia(\"(min-width: 640px)\");\n  mq.addEventListener(\"change\", notify);\n  return () => mq.removeEventListener(\"change\", notify);\n}\nconst useWide = () => useSyncExternalStore(subscribeWide, () => window.matchMedia(\"(min-width: 640px)\").matches, () => true);\n\nfunction subscribeNothing() {\n  return () => {};\n}\nconst useIsMac = () => useSyncExternalStore(subscribeNothing, () => /mac|iphone|ipad/i.test(navigator.platform || navigator.userAgent), () => true);\n\nexport type DateRangePickerProps = Omit<React.ComponentProps<\"button\">, \"value\" | \"defaultValue\" | \"onChange\" | \"children\"> & {\n  value?: CompleteRange | null;\n  defaultValue?: CompleteRange | null;\n  /** Called on Apply. Picking inside the popover is a draft until then. */\n  onValueChange?: (range: CompleteRange | null) => void;\n  /** Shortcuts down the side. false hides the column. */\n  presets?: DateRangePreset[] | false;\n  min?: Date;\n  max?: Date;\n  /** Longest range allowed, in days. */\n  maxDays?: number;\n  isDateDisabled?: (date: Date) => boolean;\n  locale?: string;\n  weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;\n  placeholder?: string;\n  size?: \"sm\" | \"md\";\n  align?: \"start\" | \"center\" | \"end\";\n  /** Submits start and end as YYYY-MM-DD under name[start] and name[end]. */\n  name?: string;\n  labels?: { apply?: string; cancel?: string; presets?: string };\n};\n\n/** Two months that show the range, keeping it in view with its end in the right-hand month. */\nfunction placement(range: DateRange | null, today: Date | null, months: number) {\n  const anchor = range ? (range.end ?? range.start) : today;\n  if (!anchor) return undefined;\n  return addMonths(startOfMonth(anchor), -(months - 1));\n}\n\nexport function DateRangePicker({\n  value: valueProp,\n  defaultValue = null,\n  onValueChange,\n  presets = defaultPresets,\n  min,\n  max,\n  maxDays,\n  isDateDisabled,\n  locale: localeProp,\n  weekStartsOn,\n  placeholder = \"Pick dates\",\n  size = \"md\",\n  align = \"start\",\n  name,\n  labels,\n  disabled,\n  className,\n  ...rest\n}: DateRangePickerProps) {\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const today = useToday();\n  const locale = useResolvedLocale(localeProp);\n  const wide = useWide();\n  const mac = useIsMac();\n  const months = wide ? 2 : 1;\n\n  const [value, setValue] = useControllableState<CompleteRange | null>({ value: valueProp, defaultValue, onChange: onValueChange });\n  const [open, setOpen] = useState(false);\n  const [draft, setDraft] = useState<DateRange | null>(null);\n  const [draftMonth, setDraftMonth] = useState<Date | undefined>(undefined);\n\n  const fmt = useMemo(() => new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", year: \"numeric\" }), [locale]);\n  const brief = useMemo(() => new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\" }), [locale]);\n  const say = (r: CompleteRange, short = false) => {\n    const f = short && today && r.start.getFullYear() === today.getFullYear() && r.end.getFullYear() === today.getFullYear() ? brief : fmt;\n    return isSameDay(r.start, r.end) ? f.format(r.start) : f.formatRange(r.start, r.end);\n  };\n\n  const refuses = (r: CompleteRange) =>\n    (min && compareDays(r.start, min) < 0) || (max && compareDays(r.end, max) > 0) || (!!maxDays && diffDays(r.start, r.end) + 1 > maxDays);\n  const presetList = presets && today ? presets.map((p) => ({ label: p.label, range: p.range(today) })) : [];\n  const presetOf = (r: DateRange | null) => presetList.find((p) => sameRange(p.range, r));\n  const valuePreset = presetOf(value);\n  const draftPreset = presetOf(draft);\n\n  function onOpenChange(next: boolean) {\n    if (next) {\n      if (disabled) return;\n      setDraft(value);\n      setDraftMonth(placement(value, today, months));\n    }\n    setOpen(next);\n  }\n\n  function choosePreset(r: CompleteRange) {\n    setDraft(r);\n    setDraftMonth(placement(r, today, months));\n  }\n\n  const complete = draft?.end ? (draft as CompleteRange) : null;\n  function apply() {\n    if (!complete) return;\n    setValue({ start: complete.start, end: complete.end });\n    setOpen(false);\n  }\n\n  const days = complete ? diffDays(complete.start, complete.end) + 1 : 0;\n  const sm = size === \"sm\";\n  const labelKey = value ? (valuePreset?.label ?? toISODate(value.start) + toISODate(value.end)) : \"empty\";\n\n  return (\n    <Popover.Root open={open} onOpenChange={onOpenChange}>\n      <Popover.Trigger\n        disabled={disabled}\n        render={<motion.button layout transition={reduce ? { duration: 0 } : spring.snappy} />}\n        aria-label={value ? `Date range, ${say(value)}${valuePreset ? ` (${valuePreset.label})` : \"\"}` : `Date range, ${placeholder}`}\n        className={cn(\n          \"group/trigger inline-flex max-w-full items-center border border-line-2 bg-raised text-fg shadow-[var(--shadow)] outline-none\",\n          \"transition-[background-color,border-color,scale] duration-150 hover:border-fg-4 hover:bg-hover active:scale-[0.98] active:duration-75\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"data-popup-open:border-fg-3 data-popup-open:bg-hover disabled:pointer-events-none disabled:opacity-50\",\n          sm ? \"h-7 gap-1.5 rounded-md px-2 text-[12.5px]\" : \"h-8 gap-2 rounded-lg px-2.5 text-[13px]\",\n          className,\n        )}\n        {...rest}\n      >\n        <motion.span layout=\"position\" className=\"flex shrink-0 text-fg-3 transition-colors group-hover/trigger:text-fg-2 group-data-popup-open/trigger:text-fg\">\n          <CalendarIcon size={sm ? 14 : 16} />\n        </motion.span>\n        {/* The trigger springs to the width of its new label; the label itself only fades, so it never stretches. */}\n        <motion.span layout=\"position\" className=\"relative flex min-w-0 items-center overflow-hidden\">\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              key={labelKey}\n              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 4, filter: \"blur(2px)\" }}\n              animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, transition: { duration: 0.1 } }}\n              transition={{ duration: 0.22, ease: ease.out }}\n              className=\"flex min-w-0 items-center gap-1.5 whitespace-nowrap\"\n            >\n              {value ? (\n                valuePreset ? (\n                  <>\n                    <span className=\"font-medium\">{valuePreset.label}</span>\n                    <span className=\"hidden truncate tabular text-fg-3 sm:inline\">{say(value, true)}</span>\n                  </>\n                ) : (\n                  <span className=\"truncate tabular font-medium\">{say(value)}</span>\n                )\n              ) : (\n                <span className=\"text-fg-3\">{placeholder}</span>\n              )}\n            </motion.span>\n          </AnimatePresence>\n        </motion.span>\n        <motion.span layout=\"position\" className=\"flex shrink-0 text-fg-3\">\n          <ChevronDown size={sm ? 12 : 14} className=\"transition-transform duration-200 ease-out-expo group-data-popup-open/trigger:rotate-180\" />\n        </motion.span>\n      </Popover.Trigger>\n\n      {name && (\n        <>\n          <input type=\"hidden\" name={`${name}[start]`} value={value ? toISODate(value.start) : \"\"} />\n          <input type=\"hidden\" name={`${name}[end]`} value={value ? toISODate(value.end) : \"\"} />\n        </>\n      )}\n\n      <Popover.Portal>\n        <Popover.Positioner side=\"bottom\" align={align} sideOffset={6} collisionPadding={8} className=\"z-(--z-popover)\">\n          <Popover.Popup\n            initialFocus={false}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n                e.preventDefault();\n                apply();\n              }\n            }}\n            className={cn(\n              \"flex max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none sm:flex-row\",\n              \"origin-[var(--transform-origin)] transition-[opacity,scale,translate] duration-200 ease-out-expo\",\n              \"data-starting-style:-translate-y-1 data-starting-style:scale-[0.97] data-starting-style:opacity-0\",\n              \"data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-out\",\n              \"motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-starting-style:scale-100\",\n            )}\n          >\n            {presetList.length > 0 && (\n              <ToggleGroup\n                aria-label={labels?.presets ?? \"Presets\"}\n                orientation={wide ? \"vertical\" : \"horizontal\"}\n                value={draftPreset ? [draftPreset.label] : []}\n                onValueChange={(v) => {\n                  const p = presetList.find((x) => x.label === v[0]);\n                  if (p) choosePreset(p.range);\n                }}\n                className={cn(\n                  \"flex shrink-0 gap-0.5 p-2\",\n                  wide\n                    ? \"w-40 flex-col border-r border-line\"\n                    : // Narrow: a scrolling row that never widens the popover, fading at the edge to say there's more.\n                      \"w-0 min-w-full overflow-x-auto overscroll-x-contain border-b border-line mask-r-from-85% [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n                )}\n              >\n                {presetList.map((p) => {\n                  const on = draftPreset?.label === p.label;\n                  return (\n                    <Toggle\n                      key={p.label}\n                      value={p.label}\n                      disabled={!!refuses(p.range)}\n                      title={say(p.range)}\n                      className={cn(\n                        \"relative flex h-8 shrink-0 items-center rounded-md px-2.5 text-left text-[13px] outline-none\",\n                        \"transition-[color,scale] duration-150 active:scale-[0.97] active:duration-75\",\n                        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-fg-3\",\n                        \"text-fg-2 hover:text-fg data-pressed:text-fg data-disabled:pointer-events-none data-disabled:text-fg-4\",\n                        !wide && \"whitespace-nowrap\",\n                      )}\n                    >\n                      {/* One pill slides to the active preset, whichever way the range was chosen. */}\n                      {on && (\n                        <motion.span\n                          layoutId={`${uid}-preset`}\n                          transition={reduce ? { duration: 0 } : spring.snappy}\n                          className=\"absolute inset-0 rounded-md bg-fg/[0.08]\"\n                        />\n                      )}\n                      <span className=\"relative\">{p.label}</span>\n                    </Toggle>\n                  );\n                })}\n              </ToggleGroup>\n            )}\n\n            <div className=\"flex min-w-0 flex-col\">\n              <Calendar\n                mode=\"range\"\n                bare\n                autoFocus\n                className=\"p-3\"\n                numberOfMonths={months}\n                month={draftMonth}\n                onMonthChange={setDraftMonth}\n                value={draft}\n                onValueChange={setDraft}\n                min={min}\n                max={max}\n                maxDays={maxDays}\n                isDateDisabled={isDateDisabled}\n                locale={locale}\n                weekStartsOn={weekStartsOn}\n              />\n              <div className=\"flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-t border-line px-3 py-2.5\">\n                <p aria-live=\"polite\" className=\"min-w-0 text-[12.5px] text-fg-3\">\n                  {complete ? (\n                    <>\n                      <span className=\"tabular text-fg\">{say(complete, true)}</span>\n                      <span className=\"text-fg-4\"> · </span>\n                      <NumberFlow value={days} className=\"tabular\" />\n                      <span className=\"ml-[0.3em]\">{days === 1 ? \"day\" : \"days\"}</span>\n                    </>\n                  ) : draft ? (\n                    <>\n                      <span className=\"tabular text-fg\">{brief.format(draft.start)} – </span>\n                      pick an end date\n                    </>\n                  ) : (\n                    \"Pick a start date\"\n                  )}\n                </p>\n                <div className=\"ml-auto flex items-center gap-2\">\n                  <Popover.Close\n                    className={cn(\n                      \"h-8 rounded-lg px-3 text-[13px] font-medium text-fg-2 outline-none transition-[background-color,color,scale] duration-150\",\n                      \"hover:bg-hover hover:text-fg active:scale-[0.97] active:duration-75\",\n                      \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                    )}\n                  >\n                    {labels?.cancel ?? \"Cancel\"}\n                  </Popover.Close>\n                  <button\n                    type=\"button\"\n                    onClick={apply}\n                    disabled={!complete}\n                    className={cn(\n                      \"inline-flex h-8 items-center gap-2 rounded-lg bg-fg px-3 text-[13px] font-medium text-frame outline-none\",\n                      \"transition-[background-color,opacity,scale] duration-150 hover:bg-fg/90 active:scale-[0.97] active:duration-75\",\n                      \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n                      \"disabled:pointer-events-none disabled:opacity-40\",\n                    )}\n                  >\n                    {labels?.apply ?? \"Apply\"}\n                    <kbd aria-hidden className=\"hidden font-sans text-[11px] text-frame/60 pointer-fine:inline\">\n                      {mac ? \"⌘↵\" : \"Ctrl ↵\"}\n                    </kbd>\n                  </button>\n                </div>\n              </div>\n            </div>\n          </Popover.Popup>\n        </Popover.Positioner>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/date-range-picker"
}
