{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-preset",
  "title": "Date Range Preset",
  "description": "A row of date-range presets — Today, Last 7 days, Month to date, Last month, Year to date — with a custom from/to range behind the last option, that answers with a relative expression like \"last7d\" instead of a pair of dates. Reach for it above anything that shows numbers for a period: an analytics or product dashboard, a revenue, sales or KPI report, billing and invoice history, usage and metering pages, a logs, events or audit-trail viewer, error and monitoring views, an admin table that filters by date, a search or order history, cohort and retention reports, attendance and timesheet summaries, a CSV or PDF export range, and the period selector beside any chart. Common asks it answers: \"date range picker react\", \"date range preset component\", \"last 7 days selector\", \"date range filter\", \"period selector dashboard\", \"relative date range react\", \"date range in URL query param\", \"shadcn date range picker\", \"shadcn date range preset\", \"react-date-range alternative\", \"react-daterange-picker alternative\", \"MUI DateRangePicker shortcuts equivalent\", \"antd RangePicker presets equivalent\", \"date range shortcuts\", \"this month last month selector\", \"analytics time range selector\", \"from to date filter component\". Official shadcn/ui has nothing for this and no combination of its parts reaches it: calendar is a react-day-picker wrapper that pulls in react-day-picker and date-fns and answers with a Date for a day, the Date Picker page is that same calendar inside a popover, and neither carries the idea of a period at all — startOfDay, endOfDay, subDays and DateRange do not appear anywhere in the library. Distinct from pulld date-input, which types one full date (and is what this composes for its custom fields), from month-picker, which chooses a single calendar month, from calendar-heatmap, which draws a year of days rather than selecting a span of them, and from weekly-hours, which sets recurring opening times rather than a one-off period. The component turns on one distinction that every hand-rolled version collapses: a relative period is an expression, not a value. Fold \"last 7 days\" into \"2026-09-17..2026-09-23\" at the moment it is clicked — which is what storing a { from: Date, to: Date } pair does — and you have written down the answer to a question nobody asked again. Share that URL and the recipient sees your week rather than theirs. Open the same saved view tomorrow and the figures have not moved, which is the single most common \"the dashboard is broken\" report there is and the hardest to see, because the page is faithfully showing the stale week it was told to. Here the value stays the string \"last7d\" all the way into the URL, the saved view and the form post, and resolveDateRange evaluates it at the moment you query. Three more things it settles that are invisible until they are wrong. The resolved range is half-open — start included, end excluded — so there is never a last instant to pick and therefore never the 23:59:59 that silently drops the final second of the period, nor the 23:59:59.999 that drops the final millisecond; the end is simply the day after the last one you want, and the summary line shows the last day actually included rather than that excluded end, because telling a reader their range ends on the 24th when the 24th is not in it is just false. Which day is \"today\" is a property of a time zone rather than of the clock, so todayIn takes an IANA zone and throws on one the runtime does not know instead of quietly falling back to whatever zone the server happens to run in — the reason a browser and the job that aggregates the rows can otherwise disagree by a day at the edges for anyone working late. And whether \"Last 7 days\" includes today is a real fork with two defensible answers, so it is written out as ordinary data in DEFAULT_PRESETS that you can replace one line at a time rather than buried in the component; there is deliberately no \"this week\", because the first day of the week is Sunday, Monday or Saturday depending on where you are and a component that quietly picked one would be wrong for much of the world without ever saying so. The preset id and a custom range share one query expression — \"last7d\" or \"2026-09-01..2026-09-30\" — so ?period= round-trips either without a second encoding to keep in sync, and parseDateRange refuses backwards ranges and impossible days like 2026-02-30 so a link a stranger edited cannot build a query. Calendar arithmetic is done without a date library: January's \"last month\" lands in the previous year on its own, February is as long as it actually was that year, and a year under 100 stays that year instead of becoming nineteen-hundred-something the way Date.UTC would have it. It is a real radiogroup with a roving tabindex — one tab stop for the whole row, then arrow keys inside it, which select as they move, with Home and End at the ends and wrapping at both, and left and right following the writing direction so they do not run backwards on an RTL page. A permanently mounted polite live region names the span in words through Intl, so the dates are announced rather than left as a visual-only state, and it stays in the accessibility tree when empty instead of being rendered along with its message. Choosing the custom option reports nothing until both dates make a range, so the dashboard behind the picker is never blanked mid-edit, and the fields open seeded with the range being looked at rather than two empty boxes. Uncontrolled, or controlled by pairing value with onValueChange; give it a name and it posts the expression with a plain form or a server action. The maths is exported too — resolveDateRange, parseDateRange, formatDateRange, todayIn, shiftDay, toCalendarDay, toPlainDate and lastIncludedDay — so a server route can resolve the same string the picker produced without reimplementing any of it. Styled entirely with shadcn tokens (primary, input, accent, ring, muted-foreground, destructive), so it follows light and dark mode, and it ships zero npm dependencies — no date library, no icon package.",
  "dependencies": [],
  "registryDependencies": [
    "https://pulld.pages.dev/r/date-input.json"
  ],
  "files": [
    {
      "path": "registry/ui/date-range-preset.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { DateInput } from \"@/registry/ui/date-input\"\n\n/**\n * A calendar day as \"YYYY-MM-DD\" — no time, no zone, no instant.\n *\n * Zero-padded to a fixed width, so `<` on two of these is chronological order and no `Date` (and\n * therefore no time zone) has to be involved in a comparison.\n */\nexport type PlainDate = string\n\n/** The year/month/day of a calendar day, with **month 1–12** as it is written. */\nexport interface CalendarDay {\n  year: number\n  month: number\n  day: number\n}\n\n/**\n * A resolved span, **half-open**: `start` is included and `end` is not.\n *\n * The inclusive alternative is the reason so many dashboards quietly lose the last few moments of\n * the final day. Written inclusively, the end of \"today\" has to be the last instant of today, and\n * every implementation picks a different one — 23:59:59, or 23:59:59.999, each of which drops\n * whatever happened in the remaining second or millisecond. Half-open has no such instant to pick:\n * the range ends where the next day begins, and `start <= row && row < end` is the whole rule.\n *\n * So `end` is the day **after** the last one you want. A single day is `{ start: \"2026-09-23\",\n * end: \"2026-09-24\" }`. To show the span to a human, use the last *included* day — see\n * `lastIncludedDay`, and note that this component's own summary line uses it, because telling a\n * reader their range ends on the 24th when the 24th is excluded is simply false.\n */\nexport interface ResolvedRange {\n  start: PlainDate\n  end: PlainDate\n}\n\n/** One option in the row: a stable id, the text on the button, and the span it means today. */\nexport interface DateRangePresetDef {\n  /**\n   * Goes in the value and therefore in the URL, so keep it short and URL-safe: letters, digits and\n   * dashes. It must not look like a custom range (see `parseDateRange`).\n   */\n  id: string\n  /** The text on the button. Supply translated strings here; nothing is localised for you. */\n  label: string\n  /** Given today as a calendar day, the half-open span the preset means. */\n  resolve: (today: CalendarDay) => ResolvedRange\n}\n\n/** What a value string turned out to be. */\nexport type ParsedDateRange =\n  | { kind: \"preset\"; preset: string }\n  | { kind: \"custom\"; from: PlainDate; to: PlainDate }\n\n/**\n * Years are shifted by this much before any arithmetic and shifted back after.\n *\n * Two problems, one fix. `Date.UTC(50, 0, 1)` is the year **1950** — the two-digit-year rule from\n * the original `Date` constructor applies to `Date.UTC` as well — and the obvious workaround\n * (build it, then `setUTCFullYear`) is wrong here, because setting the year *after* normalisation\n * loses a year that normalising rolled over: month 0 of 2026 is December **2025**, and re-stamping\n * the year afterwards makes it December 2026.\n *\n * Adding a constant first sidesteps both: the arithmetic happens far away from the two-digit\n * window, and the rollover is already folded into the year that comes back. It has to be a\n * multiple of 400 so the Gregorian leap-year pattern is identical before and after the shift,\n * which keeps 29 February exactly where it was.\n */\nconst YEAR_SHIFT = 4000\n\n/** Anchored, so \"2026-09-23T00:00:00Z\" is rejected rather than half-read as a day. */\nconst PLAIN_DATE_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/** \"2026-09-01..2026-09-30\" — both days as written, both included. */\nconst CUSTOM_RANGE_PATTERN = /^(\\d{4}-\\d{2}-\\d{2})\\.\\.(\\d{4}-\\d{2}-\\d{2})$/\n\n/** A preset id: URL-safe, and shaped so it can never be confused with a custom range. */\nconst PRESET_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9-]*$/\n\nconst pad = (value: number, width: number) => String(value).padStart(width, \"0\")\n\n/**\n * Rolls out-of-range month and day components into a real calendar day.\n *\n * This is what makes the preset table below readable: \"the first of last month\" can be written as\n * `{ year, month: month - 1, day: 1 }` and January's month 0 becomes December of the previous year\n * on its own, with no branch for the year boundary — the branch nobody writes a test for and\n * everybody gets wrong every January.\n */\nfunction normalizeDay(day: CalendarDay): CalendarDay {\n  const at = new Date(Date.UTC(day.year + YEAR_SHIFT, day.month - 1, day.day))\n  return {\n    year: at.getUTCFullYear() - YEAR_SHIFT,\n    month: at.getUTCMonth() + 1,\n    day: at.getUTCDate(),\n  }\n}\n\n/**\n * `{ year, month, day }` for a \"YYYY-MM-DD\" string, or null when it is not one.\n *\n * Impossible dates are rejected rather than rolled: \"2026-02-30\" normalises to 2 March, which no\n * longer matches what was written, and that mismatch is the whole check — cheaper than a table of\n * month lengths and correct about leap years for free. Compared as one round-tripped string rather\n * than field by field, because a field-by-field test reads as three checks when it is only ever\n * one: a day that overflows its month always moves the month too, so the day clause could never\n * fire on its own and would sit there looking load-bearing.\n */\nexport function toCalendarDay(date: PlainDate): CalendarDay | null {\n  const parsed = PLAIN_DATE_PATTERN.exec(date)\n  if (!parsed) return null\n  const candidate = {\n    year: Number(parsed[1]),\n    month: Number(parsed[2]),\n    day: Number(parsed[3]),\n  }\n  if (toPlainDate(candidate) !== date) return null\n  return candidate\n}\n\n/** \"YYYY-MM-DD\" for a calendar day, rolling any out-of-range components first. */\nexport function toPlainDate(day: CalendarDay): PlainDate {\n  const normalized = normalizeDay(day)\n  return `${pad(normalized.year, 4)}-${pad(normalized.month, 2)}-${pad(normalized.day, 2)}`\n}\n\n/** The day `days` later (or earlier, for a negative count), crossing months and years. */\nexport function shiftDay(day: CalendarDay, days: number): CalendarDay {\n  return normalizeDay({ ...day, day: day.day + days })\n}\n\n/**\n * The last day a half-open range actually includes — what to show a reader.\n *\n * `{ start: \"2026-09-17\", end: \"2026-09-24\" }` covers the 17th through the **23rd**, and printing\n * the raw `end` would tell them otherwise.\n */\nexport function lastIncludedDay(range: ResolvedRange): PlainDate {\n  const end = toCalendarDay(range.end)\n  return end ? toPlainDate(shiftDay(end, -1)) : range.end\n}\n\n/**\n * Today's date in a given time zone, as a plain day.\n *\n * \"Today\" is not a property of the moment, it is a property of where you are standing: at 22:00 in\n * Los Angeles it is already tomorrow in Berlin. So a range picker has to be told, or it silently\n * picks the zone of whichever machine happened to evaluate it — and a dashboard where the browser\n * says one day and the server that aggregates the rows says another is off by a day at the edges\n * for everyone who works late.\n *\n * Throws `RangeError` for a time zone the runtime does not know, exactly as `Intl` does. That is\n * the right failure: a mistyped IANA name is a bug in the calling code, and a silent fall back to\n * the machine's own zone would hide it behind numbers that merely look a little wrong.\n *\n * The calendar is pinned to Gregorian rather than left to the locale, since every piece of\n * arithmetic in this file is Gregorian; without the `-u-ca-gregory` a locale whose default\n * calendar is not (ar-SA, fa-IR) would hand back a year from a different era entirely.\n */\nexport function todayIn(timeZone?: string, now: Date = new Date()): PlainDate {\n  const parts = new Intl.DateTimeFormat(\"en-US-u-ca-gregory\", {\n    timeZone,\n    year: \"numeric\",\n    month: \"2-digit\",\n    day: \"2-digit\",\n  }).formatToParts(now)\n  const read = (type: string) => parts.find((part) => part.type === type)?.value ?? \"\"\n  return `${read(\"year\").padStart(4, \"0\")}-${read(\"month\")}-${read(\"day\")}`\n}\n\nconst span = (start: CalendarDay, end: CalendarDay): ResolvedRange => ({\n  start: toPlainDate(start),\n  end: toPlainDate(end),\n})\n\n/**\n * The options offered when none are supplied — and the place the \"does it include today?\"\n * question is answered out loud.\n *\n * Every one of these windows **includes today**, so \"Last 7 days\" on the 23rd is the 17th through\n * the 23rd. That is one of two live conventions and the other is defensible: ending the window\n * yesterday means the figure stops moving while people are looking at it, which is what a\n * finance team usually wants. Neither is a default worth hiding, so it is written here as\n * ordinary data you can replace rather than buried in the component:\n *\n * ```tsx\n * const throughYesterday = DEFAULT_PRESETS.map((preset) =>\n *   preset.id === \"last7d\"\n *     ? { ...preset, resolve: (t) => ({ start: toPlainDate(shiftDay(t, -7)), end: toPlainDate(t) }) }\n *     : preset\n * )\n * ```\n *\n * There is deliberately no \"this week\": the first day of the week is Sunday, Monday or Saturday\n * depending on where you are, and a component that quietly assumes one would be wrong for a large\n * part of the world without ever saying so. Add it with the week start your product has already\n * decided on, the same way as above.\n */\nexport const DEFAULT_PRESETS: readonly DateRangePresetDef[] = [\n  { id: \"today\", label: \"Today\", resolve: (t) => span(t, shiftDay(t, 1)) },\n  { id: \"yesterday\", label: \"Yesterday\", resolve: (t) => span(shiftDay(t, -1), t) },\n  { id: \"last7d\", label: \"Last 7 days\", resolve: (t) => span(shiftDay(t, -6), shiftDay(t, 1)) },\n  { id: \"last30d\", label: \"Last 30 days\", resolve: (t) => span(shiftDay(t, -29), shiftDay(t, 1)) },\n  { id: \"last90d\", label: \"Last 90 days\", resolve: (t) => span(shiftDay(t, -89), shiftDay(t, 1)) },\n  {\n    id: \"mtd\",\n    label: \"Month to date\",\n    resolve: (t) => span({ ...t, day: 1 }, shiftDay(t, 1)),\n  },\n  {\n    id: \"lastMonth\",\n    label: \"Last month\",\n    // month - 1 is month 0 every January, which normalizeDay turns into the previous December.\n    resolve: (t) => span({ year: t.year, month: t.month - 1, day: 1 }, { ...t, day: 1 }),\n  },\n  {\n    id: \"ytd\",\n    label: \"Year to date\",\n    resolve: (t) => span({ year: t.year, month: 1, day: 1 }, shiftDay(t, 1)),\n  },\n]\n\n/**\n * What a value string is, without deciding what it means.\n *\n * Returns null for anything malformed, for an impossible date, and for a custom range written\n * backwards — a value arriving from a URL is a string a stranger can edit, and \"start after end\"\n * is a range no query should be built from. Callers treat null as \"nothing selected\" and fall\n * back to their own default rather than showing a person somebody else's broken link.\n *\n * Whether a preset id is one that *exists* is deliberately not checked here: that depends on the\n * preset list, which is `resolveDateRange`'s business.\n */\nexport function parseDateRange(value: string): ParsedDateRange | null {\n  const custom = CUSTOM_RANGE_PATTERN.exec(value)\n  if (custom) {\n    const from = custom[1]\n    const to = custom[2]\n    if (!toCalendarDay(from) || !toCalendarDay(to)) return null\n    if (from > to) return null\n    return { kind: \"custom\", from, to }\n  }\n  if (PRESET_ID_PATTERN.test(value)) return { kind: \"preset\", preset: value }\n  return null\n}\n\n/**\n * The half-open span a value means **today**, or null if it means nothing.\n *\n * This is the function the whole component exists to make necessary. A relative range is an\n * expression, not a pair of dates, and the difference only shows up later: fold \"last 7 days\" into\n * \"2026-09-17..2026-09-23\" at the moment it is clicked and you have written down an answer to a\n * question nobody asked again. Share that URL and the recipient sees your week, not theirs. Open\n * the same bookmark tomorrow and the numbers have not moved — the single most common \"the\n * dashboard is broken\" report there is, and nothing about it looks broken, because the page is\n * faithfully showing the stale week it was told to.\n *\n * So the value stays `\"last7d\"` all the way into the URL and the saved view, and this runs at the\n * moment the data is fetched, against the `today` the caller supplies:\n *\n * ```ts\n * const range = resolveDateRange(searchParams.get(\"period\") ?? \"last7d\", {\n *   today: todayIn(\"America/New_York\"),\n * })\n * // → { start: \"2026-09-17\", end: \"2026-09-24\" }  — end excluded\n * ```\n *\n * `today` is required rather than defaulted for the reason spelled out on `todayIn`: which day it\n * is depends on a zone, and the one the server happens to run in is rarely the one the numbers are\n * supposed to be in. Making it an argument forces that choice to be made once, visibly, instead of\n * differing between the browser and the job that aggregates the rows.\n */\nexport function resolveDateRange(\n  value: string,\n  options: { today: PlainDate; presets?: readonly DateRangePresetDef[] }\n): ResolvedRange | null {\n  const parsed = parseDateRange(value)\n  if (!parsed) return null\n  if (parsed.kind === \"custom\") {\n    const to = toCalendarDay(parsed.to)\n    if (!to) return null\n    // The string carries the last day the person means; the range excludes its end, so the day\n    // after is what goes out. Without this, every custom range is silently one day short.\n    return { start: parsed.from, end: toPlainDate(shiftDay(to, 1)) }\n  }\n  const today = toCalendarDay(options.today)\n  if (!today) return null\n  const preset = (options.presets ?? DEFAULT_PRESETS).find((p) => p.id === parsed.preset)\n  return preset ? preset.resolve(today) : null\n}\n\n/**\n * The span in words — \"Sep 17 – 23, 2026\" — or \"\" when the value resolves to nothing.\n *\n * Always reads the last *included* day, never the excluded end.\n */\nexport function formatDateRange(\n  value: string,\n  options: { today: PlainDate; presets?: readonly DateRangePresetDef[]; locale?: string }\n): string {\n  const range = resolveDateRange(value, options)\n  if (!range) return \"\"\n  const start = toCalendarDay(range.start)\n  const last = toCalendarDay(lastIncludedDay(range))\n  if (!start || !last) return \"\"\n\n  // Built as UTC and formatted as UTC — the pair is what makes this safe. Either half on its own\n  // is the classic off-by-one-day: a UTC instant formatted in the reader's zone shows the previous\n  // evening for anyone west of Greenwich.\n  // Both components are already known to be in range here (they came back from toCalendarDay), so\n  // nothing can roll over and the year can safely be stamped on afterwards — which is what keeps a\n  // year under 100 out of the two-digit window that would otherwise read 50 as 1950.\n  const asDate = (day: CalendarDay) => {\n    const at = new Date(Date.UTC(2000, day.month - 1, day.day))\n    at.setUTCFullYear(day.year)\n    return at\n  }\n  const formatter = new Intl.DateTimeFormat(options.locale, {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n    timeZone: \"UTC\",\n  })\n  if (range.start === lastIncludedDay(range)) return formatter.format(asDate(start))\n\n  // formatRange collapses the parts the two dates share (\"Sep 17 – 23, 2026\") and puts the dash\n  // where the locale wants it. It is cast rather than called directly because this registry\n  // targets lib ES2020, where it is not in the type surface yet, and feature-detected because a\n  // runtime that predates it should still render a readable range rather than crash.\n  const withRange = formatter as Intl.DateTimeFormat & {\n    formatRange?: (start: Date, end: Date) => string\n  }\n  if (typeof withRange.formatRange === \"function\") {\n    return withRange.formatRange(asDate(start), asDate(last))\n  }\n  return `${formatter.format(asDate(start))} – ${formatter.format(asDate(last))}`\n}\n\nexport interface DateRangePresetProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"div\">,\n    \"onChange\" | \"value\" | \"defaultValue\" | \"children\"\n  > {\n  /**\n   * Controlled value: a preset id (\"last7d\") or a custom range (\"2026-09-01..2026-09-30\").\n   *\n   * This string is the whole state. Put it in the URL as it stands — `?period=last7d` — and read\n   * it back with `resolveDateRange` at the moment you query.\n   */\n  value?: string\n  /** Starting value for an uncontrolled picker. Ignored once `value` is passed. */\n  defaultValue?: string\n  /** Called with the new value string. Not called while a custom range is half-typed. */\n  onValueChange?: (value: string) => void\n  /** The options to offer. Defaults to `DEFAULT_PRESETS`; see its note on windows and week starts. */\n  presets?: readonly DateRangePresetDef[]\n  /**\n   * Which day is \"today\", as \"YYYY-MM-DD\".\n   *\n   * Pass it and the component stops reading the clock, which is what makes a server render\n   * deterministic. Leave it out and it is resolved after mount from `timeZone`, so the server's\n   * output and the browser's first paint cannot disagree about the date.\n   *\n   * It is read once per render rather than watched: a page left open across midnight keeps showing\n   * yesterday's dates in the summary line until something re-renders it. The *value* is unaffected\n   * — that is the point of keeping it relative — so the data stays correct as long as you resolve\n   * at query time. If the label itself has to tick over, own `today` in state and update it.\n   */\n  today?: PlainDate\n  /** IANA zone deciding which day is today, e.g. \"America/New_York\". Defaults to the runtime's. */\n  timeZone?: string\n  /** BCP-47 tag for the summary line and the custom fields. Defaults to the browser's own locale. */\n  locale?: string\n  /** Earliest day the custom fields accept, as \"YYYY-MM-DD\". Presets are not clamped. */\n  min?: PlainDate\n  /** Latest day the custom fields accept, as \"YYYY-MM-DD\". Presets are not clamped. */\n  max?: PlainDate\n  /** Drops the custom option, leaving only the presets. */\n  allowCustom?: boolean\n  /** Disables every control. */\n  disabled?: boolean\n  /** Submits the value with a surrounding form, through a hidden input. */\n  name?: string\n  /** Accessible name of the option row. */\n  label?: string\n  /** Text on the option that reveals the two date fields. */\n  customLabel?: string\n  /** Label on the first date field. */\n  fromLabel?: string\n  /** Label on the second date field. */\n  toLabel?: string\n  /** Shown when the second date is earlier than the first. */\n  invalidRangeMessage?: string\n}\n\n/**\n * A row of range presets — Today, Last 7 days, Month to date — with a custom range behind the last\n * option.\n *\n * ```tsx\n * const [period, setPeriod] = React.useState(\"last7d\")\n *\n * return <DateRangePreset value={period} onValueChange={setPeriod} />\n * // period is \"last7d\", not a pair of dates. Resolve it where you fetch:\n * // const { start, end } = resolveDateRange(period, { today: todayIn() })!\n * ```\n *\n * The value stays an expression so a shared link and a reopened bookmark mean the same thing\n * tomorrow that they meant today; see `resolveDateRange`.\n */\nexport const DateRangePreset = React.forwardRef<HTMLDivElement, DateRangePresetProps>(\n  function DateRangePreset(\n    {\n      className,\n      value: valueProp,\n      defaultValue = \"\",\n      onValueChange,\n      presets = DEFAULT_PRESETS,\n      today: todayProp,\n      timeZone,\n      locale,\n      min,\n      max,\n      allowCustom = true,\n      disabled,\n      name,\n      label = \"Date range\",\n      customLabel = \"Custom\",\n      fromLabel = \"From\",\n      toLabel = \"To\",\n      invalidRangeMessage = \"The end date is before the start date.\",\n      ...props\n    },\n    ref\n  ) {\n    const summaryId = React.useId()\n\n    /**\n     * Guards the one thing here that comes from the machine rather than from props: which day it\n     * is. Until mount the summary line is blank rather than wrong, because the server's clock and\n     * zone are not the reader's. Passing `today` skips the swap entirely.\n     */\n    const [mounted, setMounted] = React.useState(false)\n    React.useEffect(() => {\n      setMounted(true)\n    }, [])\n\n    const isControlled = valueProp !== undefined\n    const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)\n    const value = isControlled ? valueProp : uncontrolledValue\n\n    const today = todayProp ?? (mounted ? todayIn(timeZone) : null)\n    const parsed = parseDateRange(value)\n\n    /**\n     * Whether the custom fields are showing.\n     *\n     * Kept apart from the value because the two genuinely differ for a while: the moment the custom\n     * option is chosen there is no custom range yet, and the old value has to stay in force —\n     * reporting \"\" or a half-typed date to the parent would blank out the dashboard behind the\n     * picker while somebody is still reaching for the second field.\n     */\n    const [customOpened, setCustomOpened] = React.useState(false)\n    const isCustom = allowCustom && (parsed?.kind === \"custom\" || customOpened)\n\n    const [draft, setDraft] = React.useState(() => ({\n      from: parsed?.kind === \"custom\" ? parsed.from : \"\",\n      to: parsed?.kind === \"custom\" ? parsed.to : \"\",\n    }))\n\n    const options = React.useMemo(\n      () => [\n        ...presets.map((preset) => ({ id: preset.id, label: preset.label, custom: false })),\n        ...(allowCustom ? [{ id: \"\\u0000custom\", label: customLabel, custom: true }] : []),\n      ],\n      [presets, allowCustom, customLabel]\n    )\n\n    const selectedPreset = parsed?.kind === \"preset\" ? parsed.preset : null\n    const checkedIndex = isCustom\n      ? options.length - 1\n      : options.findIndex((option) => !option.custom && option.id === selectedPreset)\n\n    const [focusedIndex, setFocusedIndex] = React.useState(() =>\n      checkedIndex >= 0 ? checkedIndex : 0\n    )\n\n    /**\n     * A value set from outside takes the row with it — including back out of the custom fields,\n     * which `customOpened` would otherwise pin open after a parent reset the range to a preset.\n     *\n     * Written as an adjustment during render rather than an effect so the row paints correctly in\n     * the same commit as the new value, with no frame showing the old selection.\n     */\n    const [lastValue, setLastValue] = React.useState(value)\n    if (value !== lastValue) {\n      setLastValue(value)\n      const next = parseDateRange(value)\n      if (next?.kind === \"custom\") {\n        setDraft({ from: next.from, to: next.to })\n      } else if (next) {\n        setCustomOpened(false)\n      }\n    }\n\n    const emit = (next: string) => {\n      if (!isControlled) setUncontrolledValue(next)\n      onValueChange?.(next)\n    }\n\n    const groupRef = React.useRef<HTMLDivElement>(null)\n    const optionRefs = React.useRef<Array<HTMLButtonElement | null>>([])\n    // Set only by keyboard navigation, so the row never steals focus on mount or on a parent's\n    // unrelated re-render — it moves focus when, and only when, the user asked it to.\n    const focusPending = React.useRef(false)\n\n    React.useEffect(() => {\n      if (!focusPending.current) return\n      focusPending.current = false\n      optionRefs.current[focusedIndex]?.focus()\n    }, [focusedIndex])\n\n    const choose = (index: number) => {\n      const option = options[index]\n      if (!option) return\n      setFocusedIndex(index)\n      if (option.custom) {\n        setCustomOpened(true)\n        // Seed the fields from the range being looked at right now, so \"custom\" starts as the\n        // dates on screen and is edited from there rather than from two empty boxes.\n        if (draft.from === \"\" && draft.to === \"\" && today) {\n          const current = resolveDateRange(value, { today, presets })\n          if (current) setDraft({ from: current.start, to: lastIncludedDay(current) })\n        }\n        return\n      }\n      setCustomOpened(false)\n      emit(option.id)\n    }\n\n    /**\n     * Takes an edit to one of the date fields and emits only once the pair is a range.\n     *\n     * `parseDateRange` is the single gate rather than a second set of checks written out here: the\n     * string it accepts is exactly the string the URL will carry, so a range this refuses to emit\n     * is one no link could have carried either.\n     */\n    const commit = (next: { from: string; to: string }) => {\n      setDraft(next)\n      // An unfinished field makes a candidate like \"..2026-01-01\", which parseDateRange refuses\n      // along with everything else it refuses — so there is no separate check for it here.\n      const candidate = `${next.from}..${next.to}`\n      if (!parseDateRange(candidate)) return\n      emit(candidate)\n    }\n\n    const moveFocusTo = (index: number) => {\n      const wrapped = (index + options.length) % options.length\n      focusPending.current = true\n      choose(wrapped)\n    }\n\n    const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n      // The row is laid out with the writing direction, so on an RTL page the option to the right\n      // of the focused one is the *previous* one. Read at event time, where there is a real element\n      // and no render to keep deterministic.\n      const rtl = groupRef.current\n        ? getComputedStyle(groupRef.current).direction === \"rtl\"\n        : false\n      const inline = rtl ? -1 : 1\n      switch (event.key) {\n        case \"ArrowRight\":\n          moveFocusTo(focusedIndex + inline)\n          break\n        case \"ArrowLeft\":\n          moveFocusTo(focusedIndex - inline)\n          break\n        case \"ArrowDown\":\n          moveFocusTo(focusedIndex + 1)\n          break\n        case \"ArrowUp\":\n          moveFocusTo(focusedIndex - 1)\n          break\n        case \"Home\":\n          moveFocusTo(0)\n          break\n        case \"End\":\n          moveFocusTo(options.length - 1)\n          break\n        default:\n          return\n      }\n      event.preventDefault()\n    }\n\n    const backwards = isCustom && draft.from !== \"\" && draft.to !== \"\" && draft.from > draft.to\n    const summary = backwards\n      ? invalidRangeMessage\n      : today\n        ? formatDateRange(value, { today, presets, locale })\n        : \"\"\n\n    return (\n      <div ref={ref} className={cn(\"w-full space-y-3\", className)} {...props}>\n        <div\n          ref={groupRef}\n          role=\"radiogroup\"\n          aria-label={label}\n          aria-describedby={summaryId}\n          onKeyDown={handleKeyDown}\n          className=\"flex flex-wrap gap-1\"\n        >\n          {options.map((option, index) => (\n            <button\n              key={option.id}\n              ref={(node) => {\n                optionRefs.current[index] = node\n              }}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={index === checkedIndex}\n              // Roving tabindex: one stop for the whole row, then the arrow keys inside it, which\n              // is what a radio group owes the keyboard. Nine tab stops is what this replaces.\n              tabIndex={index === focusedIndex ? 0 : -1}\n              disabled={disabled}\n              onClick={() => choose(index)}\n              onFocus={() => setFocusedIndex(index)}\n              className={cn(\n                \"inline-flex h-8 items-center justify-center rounded-md border px-3 text-sm font-normal transition-colors\",\n                \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                \"disabled:pointer-events-none disabled:opacity-50\",\n                index === checkedIndex\n                  ? \"border-primary bg-primary font-medium text-primary-foreground hover:bg-primary/90\"\n                  : \"border-input bg-transparent hover:bg-accent hover:text-accent-foreground\"\n              )}\n            >\n              {option.label}\n            </button>\n          ))}\n        </div>\n\n        {isCustom ? (\n          <div className=\"flex flex-wrap items-end gap-3\">\n            <div className=\"space-y-1\">\n              <div className=\"text-xs font-medium text-muted-foreground\">{fromLabel}</div>\n              <DateInput\n                aria-label={fromLabel}\n                value={draft.from}\n                onChange={(from) => commit({ ...draft, from })}\n                min={min}\n                max={max}\n                locale={locale}\n                disabled={disabled}\n              />\n            </div>\n            <div className=\"space-y-1\">\n              <div className=\"text-xs font-medium text-muted-foreground\">{toLabel}</div>\n              <DateInput\n                aria-label={toLabel}\n                value={draft.to}\n                onChange={(to) => commit({ ...draft, to })}\n                min={min}\n                max={max}\n                locale={locale}\n                disabled={disabled}\n              />\n            </div>\n          </div>\n        ) : null}\n\n        {/*\n          Permanently mounted, and never wrapped in a condition or given `hidden`. A live region\n          has to be in the accessibility tree *before* the text it will announce arrives; one that\n          appears along with its message is a region nobody was listening to, which reads exactly\n          like silence. Empty is fine — present is the part that matters.\n        */}\n        <p\n          id={summaryId}\n          role=\"status\"\n          aria-live=\"polite\"\n          className={cn(\n            \"min-h-5 text-sm tabular-nums\",\n            backwards ? \"text-destructive\" : \"text-muted-foreground\"\n          )}\n        >\n          {summary}\n        </p>\n\n        {/* Lets the picker post with a plain form or a server action, with no state plumbing. */}\n        {name ? <input type=\"hidden\" name={name} value={value} /> : null}\n      </div>\n    )\n  }\n)\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 103 components: https://pulld.pages.dev/?utm_source=cli"
}
