# Date range picker

A button that opens a list of time windows ("Last 24 hours", "Last 7 days") and a Custom one picked on two calendars. Nothing is chosen until the person picks. Each preset prints the exact window beside its words, worked out from `now`, and the button reads the preset and its window, so nobody has to guess what "last 7 days" covered.

A preset is a rolling window: the value keeps its `preset` id, and its `from` and `to` are the instants when it was picked. Work the window out again from your own clock when you query. A custom window runs from the start of its first day to the end of its last, or between the times picked with `withTime`, in LocaleProvider's `timeZone` when it has one and the device's otherwise; Apply stays locked, saying why, until both days are picked and the end is not before the start.

With `available`, the span the data covers, a window wider than the data is narrowed to it and the picker says so under the button: the value's `from` and `to` are what's shown, and `requested` is what was asked for (with its `preset`, which the narrowed value no longer carries); the button prints the narrowed range without the preset's words. The same goes for a window that reaches the picker wider than the data, as a controlled `value`, a `defaultValue`, or when `available` narrows after a pick: the picker shows it narrowed, with the same note, and leaves the value you hold as it is until the person picks again.

- Registry name: `date-range-picker` (`@candor/date-range-picker`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/date-range-picker

## Install

```bash
npx candor-ui add date-range-picker
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/date-range-picker`.
It brings in: button, calendar, locale, locked, popover, time-input, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { DateRangePicker, type DateRangeValue } from "@/components/candor/ui/date-range-picker"

// Runs are kept for 14 days: a wider window is narrowed to them, and the picker says so.
const kept = 14 * 24 * 60 * 60 * 1000

// Each preset prints the exact window beside its words. A preset keeps its id, so work the rolling window out again
// from your own clock when you query. A preset narrowed to the data keeps its id on `requested` instead.
export default function Example() {
  const [picked, setPicked] = React.useState<DateRangeValue | undefined>(undefined)
  const [available] = React.useState(() => {
    const now = new Date()
    return { from: new Date(now.getTime() - kept), to: now }
  })
  return (
    <div className="flex flex-col items-start gap-2">
      <DateRangePicker aria-label="Runs started" value={picked ?? null} onValueChange={setPicked} available={available} withTime clearable />
      <p className="text-sm text-muted-foreground">
        {!picked
          ? "No window chosen: every run."
          : picked.preset
            ? `Rolling window: ${picked.preset}`
            : picked.requested?.preset
              ? `Asked for ${picked.requested.preset}, narrowed to the 14 days kept`
              : "A fixed window"}
      </p>
    </div>
  )
}
```

## <DateRangePicker>

Props:

- `value?: DateRangeValue | null | undefined`: The chosen window, when you control it. `null` is nothing chosen.
- `defaultValue?: DateRangeValue | undefined`: The window chosen at first, when you don't control it. Left out, nothing is chosen.
- `onValueChange?: ((value: DateRangeValue | undefined) => void) | undefined`: Called with the window a person picks, or `undefined` when they clear it.
- `presets?: DateRangePreset[] | undefined`: The rolling windows to offer. Defaults to the last hour, 24 hours, 7 days and 30 days. An empty list offers only Custom.
- `custom?: boolean | undefined` (default `true`): Offers a Custom window picked on two calendars. Defaults to true.
- `withTime?: boolean | undefined` (default `false`): A custom window takes a start and end time as well as days.
- `clearable?: boolean | undefined` (default `false`): Shows a "Clear" button once a window is chosen.
- `available?: DateRange | undefined`: The span the data covers. A window picked wider than it is narrowed to it (the value keeps the one asked for as `requested`), and the picker says "Data exists from … to …; showing that." under the button and in the list. A `value` or `defaultValue` wider than it, or one left wider when it narrows, is shown narrowed with the same note, without a call to `onValueChange`.
- `now?: (() => Date) | undefined`: The clock presets are worked out from. Defaults to the device's time when the list opens; pass your server's for the same answer everywhere.
- `locked?: string | null | undefined`: Why the picker can't be used right now. The button stays focusable and says why.
- `placeholder?: string | undefined`: Text on the button while nothing is chosen. Defaults to "Pick a time window".
- `id?: string | undefined`: The button's id, for a `<label htmlFor>` or `Field` to point at.
- `className?: string | undefined`: Classes for the button.
- `"aria-label"?: string | undefined`: The button's accessible name when it needs one beyond its text (the field's label).
- `"aria-describedby"?: string | undefined`: Ids of the text that describes the window: a hint, an error. `Field` sets it.
- `"aria-invalid"?: boolean | "true" | "false" | undefined`: The window has an error: the button is outlined in the destructive colour. `Field` sets it.
- `"aria-required"?: boolean | "true" | "false" | undefined`: A window must be chosen. `Field` sets it from `required`.

## Functions

### clampRange

```ts
clampRange(value: DateRangeValue, available: DateRange | undefined)
```

A window narrowed to the data that exists: `from` and `to` inside `available`, with the window asked for (and its preset) kept as `requested` and `preset` dropped, since the narrowed window isn't the preset's. A window inside it, or one that holds none of it (narrowing would turn it inside out), is left as it is.

### useDefaultRangePresets

```ts
useDefaultRangePresets()
```

The presets used when you give none, in the locale's words: the last hour, 24 hours, 7 days and 30 days.

### useDateRangeText

```ts
useDateRangeText()
```

A window as text in the locale: "2 Nov 2026 – 9 Nov 2026", with times when the window is shorter than two days or doesn't start and end on whole days. Pass a preset's label to lead with it: "Last 7 days: 2 Nov 2026 – 9 Nov 2026".

## Strings (`dateRangePicker`)

The fixed words, English by default. Override any of them under `dateRangePicker` in `<LocaleProvider strings={{ dateRangePicker: { … } }}>`; the table is exported as `dateRangePickerStrings`.

- `placeholder`: Pick a time window
- `presets`: Time windows
- `lastHour`: Last hour
- `last24Hours`: Last 24 hours
- `last7Days`: Last 7 days
- `last30Days`: Last 30 days
- `custom`: Custom
- `from`: From
- `to`: To
- `startTime`: Start time
- `endTime`: End time
- `apply`: Apply
- `clear`: Clear
- `range`: {from} – {to}
- `labelled`: {label}: {range}
- `needBoth`: Pick a start and an end day first.
- `endBeforeStart`: The end is before the start.
- `clamped`: Data exists from {from} to {to}; showing that.
- `availableSpan`: Data exists from {from} to {to}.
- `outside`: No data exists in this window; data exists from {from} to {to}.

## Types

### DateRange

A window of time, from one instant to another.

```ts
{
  /** The start, included. */
  from: Date;
  /** The end, included. */
  to: Date;
}
```

### DateRangeValue

A chosen window: the instants, and the preset it came from when it was one.

```ts
DateRange & {
  /**
   * The preset's `id` when the window is a rolling one ("Last 7 days"), so the app can work it out again from its own
   * clock. Absent for a custom window, and for one narrowed to the data: `from` and `to` are then no longer the
   * preset's window, so the preset is kept on `requested` instead.
   */
  preset?: string | undefined;
  /**
   * The window the person asked for, with its `preset` when it was one, when it was wider than the picker's `available`
   * data and `from` and `to` were narrowed to it. Absent when nothing was narrowed.
   */
  requested?: (DateRange & {
    preset?: string | undefined;
  }) | undefined;
}
```

### DateRangePreset

A rolling window that ends now: "Last 24 hours".

```ts
{
  /** Kept on the value as `preset`. */
  id: string;
  /** The words: "Last 24 hours". */
  label: string;
  /** How far back the window starts, in milliseconds. */
  duration: number;
}
```

## Motion

- **tint** (tint): A preset's background changes when the pointer is over it, and a ring fades in when it takes keyboard focus.

Set with `--cd-motion-date-range-picker-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
