{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "title": "Date Range Picker",
  "description": "Presets, date inputs, and a keyboard-navigable calendar for local date ranges.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "toggle",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/date-range-picker.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const DATE_RANGE_PICKER_VARIANTS = [\"card\", \"split\", \"compact\"] as const;\nexport type DateRangePickerVariant = (typeof DATE_RANGE_PICKER_VARIANTS)[number];\n/** Local calendar dates in YYYY-MM-DD format; no UTC conversion. */\nexport type DateRange = { start: string; end: string };\nexport type DateRangePickerProps = Omit<ComponentProps<\"div\">, \"defaultValue\"> & {\n  variant?: DateRangePickerVariant;\n  value?: DateRange;\n  defaultValue?: DateRange;\n  onValueChange?: (value: DateRange) => void;\n  min?: string;\n  max?: string;\n  disabled?: boolean;\n};\nfunction dateKey(date: Date) {\n  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n}\nfunction parseDate(value: string) {\n  const year = Number(value.slice(0, 4));\n  const month = Number(value.slice(5, 7));\n  const day = Number(value.slice(8, 10));\n  return new Date(year, month - 1, day, 12);\n}\nfunction validDate(value: string) {\n  return /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && dateKey(parseDate(value)) === value;\n}\nfunction inBounds(value: string, min?: string, max?: string) {\n  return validDate(value) && (!min || value >= min) && (!max || value <= max);\n}\nfunction clampDate(value: string, min?: string, max?: string) {\n  return min && value < min ? min : max && value > max ? max : value;\n}\nfunction moveDate(value: string, days: number) {\n  const date = parseDate(value);\n  date.setDate(date.getDate() + days);\n  return dateKey(date);\n}\nfunction moveMonth(value: string, months: number) {\n  const date = parseDate(value);\n  const day = date.getDate();\n  date.setDate(1);\n  date.setMonth(date.getMonth() + months);\n  const last = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();\n  date.setDate(Math.min(day, last));\n  return dateKey(date);\n}\ntype RangeContext = {\n  id: string;\n  variant: DateRangePickerVariant;\n  value: DateRange;\n  change: (value: DateRange) => void;\n  min?: string;\n  max?: string;\n  disabled: boolean;\n};\nconst Context = createContext<RangeContext | null>(null);\nconst dateRangePickerVariants = cva(\n  \"grid min-w-0 border bg-card text-[13px]/[18px] text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        card: \"gap-3.5 rounded-[14px] p-4\",\n        split: \"gap-3.5 rounded-[14px] p-4\",\n        compact: \"gap-2.5 rounded-xl p-3\",\n      },\n    },\n  },\n);\nconst focusRing =\n  \"focus-visible:relative focus-visible:z-1 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring\";\n// Shared ghost treatment for presets, month navigation, and the clear button. The neutral\n// hover, ring and pointer-event resets replace the Button/Toggle base so only enabled\n// controls react and disabled ones keep the not-allowed cursor.\nconst ghostButton = cn(\n  \"cursor-pointer border-0 bg-transparent text-muted-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_var(--ease-out-quint)] hover:bg-transparent hover:text-muted-foreground enabled:hover:bg-accent enabled:hover:text-foreground enabled:active:scale-[0.97] focus-visible:ring-0 focus-visible:outline-solid disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-35 motion-reduce:transition-none dark:hover:bg-transparent\",\n  focusRing,\n);\nfunction useRange() {\n  const context = useContext(Context);\n  if (!context) throw new Error(\"DateRangePicker children must be used within DateRangePicker\");\n  return context;\n}\nexport function DateRangePicker({\n  variant = \"card\",\n  value,\n  defaultValue = { start: \"\", end: \"\" },\n  onValueChange,\n  min,\n  max,\n  disabled = false,\n  children,\n  className,\n  ...props\n}: DateRangePickerProps) {\n  const id = useId();\n  const [internal, setInternal] = useState(defaultValue);\n  if ((min && !validDate(min)) || (max && !validDate(max)) || (min && max && min > max))\n    throw new Error(\"DateRangePicker requires valid, ordered YYYY-MM-DD bounds\");\n  const range = value ?? internal;\n  const change = (next: DateRange) => {\n    if (\n      disabled ||\n      (next.start && !inBounds(next.start, min, max)) ||\n      (next.end && (!next.start || !inBounds(next.end, min, max) || next.end < next.start))\n    )\n      return;\n    if (value === undefined) setInternal(next);\n    onValueChange?.(next);\n  };\n  return (\n    <Context.Provider value={{ id, variant, value: range, change, min, max, disabled }}>\n      <div\n        data-slot=\"date-range-picker\"\n        className={cn(dateRangePickerVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\nexport function DateRangePickerInputs({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"date-range-picker-inputs\"\n      className={cn(\"flex flex-wrap gap-2\", className)}\n      {...props}\n    />\n  );\n}\nexport function DateRangePickerInput({\n  boundary,\n  children,\n  className,\n  onChange,\n  ...props\n}: Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"type\" | \"min\" | \"max\" | \"id\"> & {\n  boundary: \"start\" | \"end\";\n}) {\n  const context = useRange();\n  const id = `${context.id}-${boundary}`;\n  const compact = context.variant === \"compact\";\n  return (\n    <Label\n      htmlFor={id}\n      data-slot=\"date-range-picker-input-label\"\n      className=\"grid min-w-0 flex-[1_1_120px] items-stretch gap-1.5 text-xs/4 font-medium text-muted-foreground select-auto\"\n    >\n      {children ?? (boundary === \"start\" ? \"Start date\" : \"End date\")}\n      <Input\n        data-slot=\"date-range-picker-input\"\n        className={cn(\n          \"box-border w-full min-w-0 border border-border bg-background px-2.5 py-0 font-normal text-foreground tabular-nums shadow-none transition-[border-color,box-shadow] duration-120 ease-[ease-out] enabled:hover:not-focus:border-border-strong focus:border-border-strong focus:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_24%,transparent)] focus:outline-none focus-visible:border-border-strong focus-visible:ring-0 disabled:pointer-events-auto disabled:opacity-100 motion-reduce:transition-none dark:bg-background [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-55\",\n          compact\n            ? \"h-[30px] rounded-lg text-[12.5px] md:text-[12.5px]\"\n            : \"h-[34px] rounded-[10px] text-[13px] md:text-[13px]\",\n          className,\n        )}\n        {...props}\n        id={id}\n        type=\"date\"\n        value={context.value[boundary]}\n        disabled={context.disabled || props.disabled}\n        min={boundary === \"end\" ? context.value.start || context.min : context.min}\n        max={context.max}\n        onChange={(event) => {\n          onChange?.(event);\n          if (event.defaultPrevented) return;\n          const date = event.target.value;\n          if (boundary === \"start\")\n            context.change({\n              start: date,\n              end: !date || date > context.value.end ? \"\" : context.value.end,\n            });\n          else context.change({ ...context.value, end: date });\n        }}\n      />\n    </Label>\n  );\n}\nexport function DateRangePickerBody({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useRange();\n  return (\n    <div\n      data-slot=\"date-range-picker-body\"\n      className={cn(\n        \"flex flex-wrap\",\n        context.variant === \"split\" ? \"flex-row\" : \"flex-col\",\n        context.variant === \"compact\" ? \"gap-2.5\" : \"gap-3.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\nexport function DateRangePickerPresets({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useRange();\n  return (\n    <div\n      data-slot=\"date-range-picker-presets\"\n      className={cn(\n        \"flex flex-wrap\",\n        context.variant === \"split\"\n          ? \"min-w-[120px] flex-col gap-0.5 border-r pr-3\"\n          : \"flex-row gap-1\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\nexport function DateRangePickerPreset({\n  value,\n  onClick,\n  className,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"value\"> & { value: DateRange }) {\n  const context = useRange();\n  const selected = value.start === context.value.start && value.end === context.value.end;\n  const allowed =\n    inBounds(value.start, context.min, context.max) &&\n    inBounds(value.end, context.min, context.max) &&\n    value.start <= value.end;\n  return (\n    <Toggle\n      data-slot=\"date-range-picker-preset\"\n      className={cn(\n        ghostButton,\n        \"h-7 min-w-0 justify-start px-3 text-left text-[12.5px] font-medium whitespace-nowrap aria-pressed:bg-accent aria-pressed:text-foreground data-[state=on]:bg-accent data-pressed:bg-accent data-[state=on]:text-foreground data-pressed:text-foreground\",\n        context.variant === \"split\" ? \"rounded-lg\" : \"rounded-full\",\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      pressed={selected}\n      disabled={context.disabled || !allowed || props.disabled}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.change(value);\n      }}\n    />\n  );\n}\nexport function DateRangePickerCalendar({\n  locale = \"en-US\",\n  className,\n  ...props\n}: ComponentProps<\"div\"> & { locale?: string }) {\n  const context = useRange();\n  const titleId = useId();\n  const ref = useRef<HTMLDivElement>(null);\n  const [focused, setFocused] = useState(() =>\n    clampDate(context.value.start || dateKey(new Date()), context.min, context.max),\n  );\n  // `direction` drives the month slide: 1 forward, -1 backward, 0 on first render.\n  const [view, setView] = useState({ month: focused.slice(0, 7), direction: 0 });\n  const month = view.month;\n  const setMonth = (next: string) =>\n    setView((current) =>\n      next === current.month ? current : { month: next, direction: next > current.month ? 1 : -1 },\n    );\n  useEffect(() => {\n    if (context.value.start && inBounds(context.value.start, context.min, context.max)) {\n      const start = context.value.start;\n      setFocused(start);\n      setView((current) =>\n        start.slice(0, 7) === current.month\n          ? current\n          : { month: start.slice(0, 7), direction: start.slice(0, 7) > current.month ? 1 : -1 },\n      );\n    }\n  }, [context.value.start, context.min, context.max]);\n  const first = parseDate(`${month}-01`);\n  const gridStart = moveDate(`${month}-01`, -first.getDay());\n  const formatter = new Intl.DateTimeFormat(locale, { dateStyle: \"full\" });\n  const navigate = (next: string, focus: boolean) => {\n    const target = clampDate(next, context.min, context.max);\n    setFocused(target);\n    setMonth(target.slice(0, 7));\n    if (focus)\n      requestAnimationFrame(() =>\n        ref.current?.querySelector<HTMLButtonElement>(`[data-date=\"${target}\"]`)?.focus(),\n      );\n  };\n  const select = (date: string) => {\n    if (!context.value.start || context.value.end || date < context.value.start)\n      context.change({ start: date, end: \"\" });\n    else context.change({ start: context.value.start, end: date });\n  };\n  return (\n    <div\n      data-slot=\"date-range-picker-calendar\"\n      className={cn(\"min-w-0 flex-[1_1_210px]\", className)}\n      {...props}\n      ref={ref}\n    >\n      <div className=\"mb-2 flex items-center justify-between\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Previous month\"\n          disabled={context.disabled || Boolean(context.min && month <= context.min.slice(0, 7))}\n          onClick={() => navigate(moveMonth(`${month}-01`, -1), false)}\n          className={monthButton}\n        >\n          <ChevronLeft size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n        </Button>\n        <span id={titleId} aria-live=\"polite\" className=\"font-medium\">\n          {new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(first)}\n        </span>\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          aria-label=\"Next month\"\n          disabled={context.disabled || Boolean(context.max && month >= context.max.slice(0, 7))}\n          onClick={() => navigate(moveMonth(`${month}-01`, 1), false)}\n          className={monthButton}\n        >\n          <ChevronRight size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n        </Button>\n      </div>\n      <table\n        aria-labelledby={titleId}\n        className=\"w-full table-fixed border-separate border-spacing-x-0 border-spacing-y-0.5\"\n      >\n        <thead>\n          <tr>\n            {Array.from({ length: 7 }, (_, day) => (\n              <th\n                scope=\"col\"\n                key={moveDate(\"2026-09-06\", day)}\n                className=\"pb-1 text-[11px] font-normal text-subtle-foreground\"\n              >\n                {new Intl.DateTimeFormat(locale, { weekday: \"short\" }).format(\n                  parseDate(moveDate(\"2026-09-06\", day)),\n                )}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody\n          key={month}\n          className={cn(\n            view.direction !== 0 &&\n              \"animate-in fade-in-0 duration-200 ease-out-quint motion-reduce:animate-none\",\n            view.direction > 0 && \"slide-in-from-right-2\",\n            view.direction < 0 && \"slide-in-from-left-2\",\n          )}\n        >\n          {Array.from({ length: 6 }, (_, week) => (\n            <tr key={moveDate(gridStart, week * 7)}>\n              {Array.from({ length: 7 }, (_, day) => {\n                const date = moveDate(gridStart, week * 7 + day);\n                const selected = Boolean(\n                  context.value.start &&\n                    date >= context.value.start &&\n                    date <= (context.value.end || context.value.start),\n                );\n                const endpoint = date === context.value.start || date === context.value.end;\n                const ranged = Boolean(\n                  context.value.end && context.value.end !== context.value.start,\n                );\n                const isStart = date === context.value.start;\n                const isEnd = date === (context.value.end || context.value.start);\n                const roundStart = !selected || (ranged ? isStart || day === 0 : true);\n                const roundEnd = !selected || (ranged ? isEnd || day === 6 : true);\n                const unavailable = context.disabled || !inBounds(date, context.min, context.max);\n                return (\n                  <td key={date} className=\"p-0\">\n                    <button\n                      type=\"button\"\n                      aria-pressed={selected}\n                      data-date={date}\n                      data-endpoint={endpoint || undefined}\n                      className={cn(\n                        \"block w-full cursor-pointer border-0 p-0 text-[12.5px] tabular-nums transition-colors duration-120 ease-[ease-out] disabled:cursor-not-allowed disabled:opacity-35 motion-reduce:transition-none\",\n                        focusRing,\n                        context.variant === \"compact\" ? \"min-h-7\" : \"min-h-8\",\n                        roundStart && \"rounded-l-lg\",\n                        roundEnd && \"rounded-r-lg\",\n                        endpoint\n                          ? \"bg-primary font-medium text-primary-foreground\"\n                          : cn(\n                              \"font-normal aria-[current=date]:font-medium aria-[current=date]:text-primary-ink\",\n                              selected\n                                ? \"bg-primary/16 enabled:hover:bg-primary/26\"\n                                : \"bg-transparent enabled:hover:bg-accent\",\n                              date.slice(0, 7) !== month\n                                ? \"text-subtle-foreground\"\n                                : \"text-foreground\",\n                            ),\n                      )}\n                      aria-label={formatter.format(parseDate(date))}\n                      aria-current={date === dateKey(new Date()) ? \"date\" : undefined}\n                      tabIndex={date === focused ? 0 : -1}\n                      disabled={unavailable}\n                      onFocus={() => setFocused(date)}\n                      onClick={() => select(date)}\n                      onKeyDown={(event) => {\n                        const offsets: Record<string, number> = {\n                          ArrowLeft: -1,\n                          ArrowRight: 1,\n                          ArrowUp: -7,\n                          ArrowDown: 7,\n                          Home: -parseDate(date).getDay(),\n                          End: 6 - parseDate(date).getDay(),\n                        };\n                        let next: string;\n                        if (event.key in offsets) next = moveDate(date, offsets[event.key] ?? 0);\n                        else if (event.key === \"PageUp\" || event.key === \"PageDown\")\n                          next = moveMonth(\n                            date,\n                            (event.key === \"PageUp\" ? -1 : 1) * (event.shiftKey ? 12 : 1),\n                          );\n                        else return;\n                        event.preventDefault();\n                        navigate(next, true);\n                      }}\n                    >\n                      {parseDate(date).getDate()}\n                    </button>\n                  </td>\n                );\n              })}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\nconst monthButton = cn(ghostButton, \"grid size-7 place-items-center rounded-lg p-0\");\nexport function DateRangePickerSummary({\n  locale = \"en-US\",\n  className,\n  ...props\n}: ComponentProps<\"p\"> & { locale?: string }) {\n  const context = useRange();\n  const formatter = new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" });\n  const format = (value: string) => formatter.format(parseDate(value));\n  return (\n    <p\n      role=\"status\"\n      data-slot=\"date-range-picker-summary\"\n      className={cn(\"m-0 text-xs/4 text-subtle-foreground tabular-nums\", className)}\n      {...props}\n    >\n      {!context.value.start\n        ? \"Choose a start date.\"\n        : !context.value.end\n          ? `${format(context.value.start)} selected. Choose an end date.`\n          : `${format(context.value.start)} to ${format(context.value.end)}`}\n    </p>\n  );\n}\nexport function DateRangePickerClear({\n  children = \"Clear dates\",\n  onClick,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useRange();\n  return (\n    <Button\n      data-slot=\"date-range-picker-clear\"\n      variant=\"ghost\"\n      className={cn(\n        ghostButton,\n        \"-ml-3 h-7 justify-self-start rounded-full px-3 py-0 text-[12.5px] font-medium has-[>svg]:px-3\",\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      disabled={context.disabled || !context.value.start || props.disabled}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.change({ start: \"\", end: \"\" });\n      }}\n    >\n      {children}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/date-range-picker.tsx"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}