{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"date-range-picker","type":"registry:ui","title":"Date Range Picker","description":"Pick a start and end day on a two-month calendar, or from shortcuts like “Last 7 days”.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/button.json","https://dashral.com/r/calendar.json","https://dashral.com/r/field.json","https://dashral.com/r/popover.json"],"files":[{"path":"registry/dashral/date-range-picker.tsx","type":"registry:file","target":"components/dashral/date-range-picker.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { CalendarIcon } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { FieldWrapper, splitFieldProps, useFieldControl, type FieldClassNames, type FieldControlProps, type FieldOwnProps } from \"./field\"\nimport { Button } from \"./button\"\nimport { Calendar, type DateRange } from \"./calendar\"\nimport { PopoverRoot, PopoverContent, PopoverTrigger } from \"./popover\"\n\nexport type DateRangePreset = { label: string; range: () => DateRange }\n\n/** Phone-sized screens (below Tailwind's sm). False while rendering on the server. */\nfunction useNarrowScreen() {\n  return React.useSyncExternalStore(\n    (onChange) => {\n      const query = window.matchMedia(\"(max-width: 639px)\")\n      query.addEventListener(\"change\", onChange)\n      return () => query.removeEventListener(\"change\", onChange)\n    },\n    () => window.matchMedia(\"(max-width: 639px)\").matches,\n    () => false\n  )\n}\n\nconst day = (offset = 0) => {\n  const d = new Date()\n  d.setHours(0, 0, 0, 0)\n  d.setDate(d.getDate() + offset)\n  return d\n}\n\n/** Today, Yesterday, Last 7 and 30 days, This month and Last month — worked out when picked, in the browser's time zone. */\nconst defaultDateRangePresets = (t: ReturnType<typeof useLocale>[\"t\"]): DateRangePreset[] => [\n  { label: t(\"today\"), range: () => ({ from: day(), to: day() }) },\n  { label: t(\"yesterday\"), range: () => ({ from: day(-1), to: day(-1) }) },\n  { label: t(\"last7Days\"), range: () => ({ from: day(-6), to: day() }) },\n  { label: t(\"last30Days\"), range: () => ({ from: day(-29), to: day() }) },\n  {\n    label: t(\"thisMonth\"),\n    range: () => {\n      const today = day()\n      return { from: new Date(today.getFullYear(), today.getMonth(), 1), to: today }\n    },\n  },\n  {\n    label: t(\"lastMonth\"),\n    range: () => {\n      const today = day()\n      return { from: new Date(today.getFullYear(), today.getMonth() - 1, 1), to: new Date(today.getFullYear(), today.getMonth(), 0) }\n    },\n  },\n]\n\ntype DateRangePickerProps = FieldOwnProps & {\n  /** Controlled value. Omit it (and use defaultValue) for an uncontrolled picker. */\n  value?: DateRange\n  defaultValue?: DateRange\n  onValueChange?: (range: DateRange | undefined) => void\n  /** Shortcuts beside the calendar; `[]` hides them. */\n  presets?: DateRangePreset[]\n  placeholder?: string\n  /** BCP 47 locale for the button label, e.g. \"id-ID\". */\n  locale?: string\n  formatOptions?: Intl.DateTimeFormatOptions\n  /** Months shown side by side on wider screens; a phone always gets one. */\n  numberOfMonths?: number\n  /** Days that can't be picked, e.g. `{ after: new Date() }` — any DayPicker matcher. */\n  disabledDays?: React.ComponentProps<typeof Calendar>[\"disabled\"]\n  /** The whole picker can't be used. */\n  disabled?: boolean\n  required?: boolean\n  id?: string\n  /** The trigger's classes. */\n  className?: string\n  \"aria-label\"?: string\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"]\n  classNames?: FieldClassNames & { trigger?: string; content?: string }\n}\n\n/** Pick a start and end day on a calendar, or from shortcuts like \"Last 7 days\". */\nfunction DateRangePicker(props: DateRangePickerProps) {\n  const [field, { required: _required, ...control }] = splitFieldProps(props)\n  return (\n    <FieldWrapper {...field}>\n      <DateRangePickerControl {...control} />\n    </FieldWrapper>\n  )\n}\n\nfunction DateRangePickerControl({\n  value,\n  defaultValue,\n  onValueChange,\n  presets: presetsProp,\n  placeholder: placeholderProp,\n  locale: localeProp,\n  formatOptions = { month: \"short\", day: \"numeric\", year: \"numeric\" },\n  numberOfMonths = 2,\n  disabledDays,\n  classNames,\n  className,\n  \"aria-label\": ariaLabel,\n  ...rest\n}: Omit<DateRangePickerProps, keyof FieldOwnProps | \"required\">) {\n  const { t, locale: contextLocale } = useLocale()\n  const presets = presetsProp ?? defaultDateRangePresets(t)\n  const locale = localeProp ?? contextLocale\n  const placeholder = placeholderProp ?? t(\"pickDateRange\")\n  const { id, disabled, ...aria } = useFieldControl<FieldControlProps>({ id: rest.id, disabled: rest.disabled, \"aria-invalid\": rest[\"aria-invalid\"] })\n  const [open, setOpen] = React.useState(false)\n  const narrow = useNarrowScreen()\n  const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue)\n  const current = value === undefined && onValueChange === undefined ? internal : value\n  // Which month the calendar shows: follows a picked preset, so \"Last month\" opens on last month.\n  const [month, setMonth] = React.useState<Date | undefined>(current?.from)\n\n  function set(range: DateRange | undefined) {\n    setInternal(range)\n    onValueChange?.(range)\n  }\n\n  const format = new Intl.DateTimeFormat(locale, formatOptions)\n  // Node and browsers put different spaces around the dash (thin or regular), which would make the server-rendered\n  // label differ from the browser's; plain spaces everywhere.\n  const plain = (text: string) => text.replace(/[\\u2009\\u202f\\u00a0]/g, \" \")\n  const label = current?.from\n    ? plain(current.to ? format.formatRange(current.from, current.to) : format.format(current.from))\n    : placeholder\n\n  return (\n    <PopoverRoot\n      open={open}\n      onOpenChange={(next) => {\n        if (next) setMonth(current?.from)\n        setOpen(next)\n      }}\n    >\n      <PopoverTrigger asChild>\n        <Button\n          id={id}\n          variant=\"outline\"\n          disabled={disabled}\n          aria-label={ariaLabel}\n          aria-describedby={aria[\"aria-describedby\"]}\n          aria-invalid={aria[\"aria-invalid\"]}\n          className={cn(\n            \"w-full justify-start font-normal aria-invalid:border-destructive sm:w-auto\",\n            // Looks like the other fields (Input, Select), not like a button.\n            \"border-input bg-transparent hover:bg-transparent focus-visible:border-ring focus-visible:ring-ring/15 motion-safe:enabled:not-aria-busy:active:scale-100\",\n            current?.from ? \"hover:text-foreground\" : \"text-muted-foreground hover:text-muted-foreground\",\n            className,\n            classNames?.trigger\n          )}\n        >\n          <CalendarIcon aria-hidden />\n          <span className=\"truncate\">{label}</span>\n        </Button>\n      </PopoverTrigger>\n      {/* Never wider or taller than the screen: it keeps 8px from the edges and scrolls if it must. */}\n      <PopoverContent\n        className={cn(\n          \"flex max-h-(--radix-popover-content-available-height) w-auto max-w-[calc(100vw-1rem)] flex-col overflow-y-auto p-0 max-sm:w-[calc(100vw-1rem)] sm:flex-row\",\n          classNames?.content\n        )}\n        align=\"start\"\n        collisionPadding={8}\n      >\n        {presets.length > 0 && (\n          // On a phone, a full-width grid of shortcuts above the calendar; beside it on wider screens.\n          <div role=\"group\" aria-label={t(\"shortcuts\")} className=\"grid grid-cols-3 gap-1 border-b p-2 sm:flex sm:flex-col sm:border-e sm:border-b-0\">\n            {presets.map((preset) => (\n              <Button\n                key={preset.label}\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"shrink-0 font-normal max-sm:px-1 sm:justify-start\"\n                onClick={() => {\n                  const range = preset.range()\n                  set(range)\n                  setMonth(range.from)\n                  setOpen(false)\n                }}\n              >\n                {preset.label}\n              </Button>\n            ))}\n          </div>\n        )}\n        <Calendar\n          mode=\"range\"\n          selected={current}\n          onSelect={set}\n          month={month}\n          onMonthChange={setMonth}\n          numberOfMonths={narrow ? 1 : numberOfMonths}\n          // On a phone the days fill the width: seven cells across the popover, at most 3rem each.\n          className=\"self-center max-sm:w-full max-sm:[--cell-size:min(3rem,calc((100vw-2.625rem)/7))] sm:self-auto\"\n          disabled={disabledDays}\n          autoFocus\n        />\n      </PopoverContent>\n    </PopoverRoot>\n  )\n}\n\nexport { DateRangePicker, defaultDateRangePresets }\n"}]}