# TimeField and TimeRangeField

> A time of day with typed input and a popup picker; the range variant pairs start and end on one row.

Category: Forms. Docs page: https://rime.mjsons.net/components/time-field

## Installation

```bash
npx shadcn@latest add KezLahd/rime/time-field
```

TimeField lets people type a time the way they say it ("3pm", "0930", "14:30") and offers a popup picker as a second path: scrollable hour, minute and AM/PM columns that auto-center on the current value. The value is "HH:MM" 24h regardless of display format, so it sorts, compares and serialises predictably.

format="12h" shows "9:30 AM" and parses am/pm; the default is 24h. step sets the minute granularity for the popup list (default 15; set to 1 for a free picker). min and max bound both the parse and the picker — a chosen time outside the bounds is kept on screen with a reason, not silently wiped. TimeRangeField pairs two TimeFields with an en dash between them and flags end at or before start as invalid so the row disagrees with itself until the parent fixes it.

## Usage

```tsx
import { TimeField, TimeRangeField, type Time, type TimeRange } from "@/components/ui";

<TimeField value={at} onChange={setAt} />
```

## Props: TimeField

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `Time \| null` |  |  |
| `onChange` (required) | `(value: Time \| null) => void` |  | null when cleared or when the typed text can't be read as a time. |
| `format` | `"12h" \| "24h"` | `"24h"` | 24h is the default. 12h shows "9:30 AM" and parses am/pm. |
| `step` | `number` | `15` | Minute granularity for the popup list (default 15). Set to 1 for a free picker. |
| `min` | `Time` |  |  |
| `max` | `Time` |  |  |
| `invalid` | `boolean` |  |  |
| `disabled` | `boolean` |  |  |
| `required` | `boolean` |  |  |
| `id` | `string` |  |  |
| `name` | `string` |  |  |
| `placeholder` | `string` |  |  |
| `aria-label` | `string` |  |  |
| `className` | `string` |  |  |
| `inline` | `boolean` | `false` | Keep the popup rendered open under the field (for docs). |

## Props: TimeRangeField

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `TimeRange` |  |  |
| `onChange` (required) | `(value: TimeRange) => void` |  |  |
| `format` | `"12h" \| "24h"` | `"24h"` |  |
| `step` | `number` | `15` |  |
| `requireEndAfterStart` | `boolean` | `true` | When true, flag an end at or before start as invalid. Default true. |
| `min` | `Time` |  |  |
| `max` | `Time` |  |  |
| `disabled` | `boolean` |  |  |
| `className` | `string` |  |  |
| `aria-label-start` | `string` |  |  |
| `aria-label-end` | `string` |  |  |

## Examples

### Open

inline shows the picker open under the field, in place.

```tsx
<TimeField inline aria-label="Opens at" value={at} onChange={setAt} format="12h" step={15} />
```

### Single time and range

```tsx
<TimeField value={at} onChange={setAt} format="12h" step={15} />
<TimeRangeField value={range} onChange={setRange} format="12h" step={30} />
```

## Theme tokens

Per-component tokens. Undeclared by default: the default below is the fallback the CSS uses. Set any of them at :root or on a wrapper to restyle every instance.

| Token | Default |
| --- | --- |
| `--field-focus-border` | `rgba(var(--rgb-brand), 0.7)` / `var(--focus-color)` |
| `--field-focus-ring` | `var(--focus-halo)` |

System tokens it reads: `--danger-ink`, `--dur`, `--ease`, `--ease-out`, `--focus-color`, `--focus-halo`, `--ink-body`, `--ink-brand`, `--ink-disabled`, `--ink-heading`, `--ink-muted`, `--line`, `--r-lg`, `--r-sm`, `--r-xs`, `--rgb-brand`, `--shadow-float`, `--surface-solid`, `--surface-sunken`, `--text-small`, `--wash-active`, `--wash-hover`, `--wash-subtle`, `--z-popover`.

## Accessibility

- The field accepts spoken-style input ("3pm", "9:30 am") and reports a reason when the text can't be read as a time, named by aria-describedby.
- The popup is a role="dialog" with hour, minute and (12h only) AM/PM listboxes; arrow keys move focus between cells and Enter picks.
- Alt+Down from the field opens the popup. Escape closes it and returns focus to the toggle, so a Modal containing the field doesn't close with it.

## Do

- Use step={15} or step={30} for scheduling flows; most times a human picks are on the quarter or half hour.
- Pass min and max for opening hours or an event window so the picker only lists times that work.

## Don't

- Don't use a free-text input for a time; the picker catches typos and ambiguous formats cheaply.

Related: https://rime.mjsons.net/components/date-field, https://rime.mjsons.net/components/date-range-filter, https://rime.mjsons.net/components/date-time-field
