<!-- Protocore Design System — TimePicker -->
# TimePicker

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/time-picker`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { TimePicker } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/time-picker

> Sunken field that opens a TimeInput plus scrollable hour / minute columns in a raised overlay for point-and-click time selection.

## When to use it

Use **TimePicker** when picking a rounded slot by eye is easier than typing — a booking time, an alarm. It still embeds a **TimeInput** for fast keyboard entry. For a bare typed field use **TimeInput**; to choose a date and time together use **DateTimePicker**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `defaultOpen` | `boolean` | no | — | Initial open state in uncontrolled mode. |
| `defaultValue` | `TimeValue \| null` | no | — | Initial time in uncontrolled mode. |
| `disabled` | `boolean` | no | — | Disable the whole control. |
| `hour12` | `boolean` | no | `false` | Use a 12-hour clock. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `locale` | `string` | no | — | BCP-47 locale for the formatted field value. |
| `minuteStep` | `number` | no | `5` | Minute (and second) column step. |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Fires when the overlay opens or closes. |
| `onValueChange` | `((value: TimeValue \| null) => void)` | no | — | Fires with the chosen time, or `null` while incomplete. |
| `open` | `boolean` | no | — | Controlled open state of the overlay. |
| `placeholder` | `string` | no | `Select time` | Text shown when no time is selected. |
| `size` | `enum` | no | `md` | Field height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `TimeValue \| null` | no | — | Controlled time. Pass `null` for no selection. |
| `withSeconds` | `boolean` | no | `false` | Include a seconds column + segment. |

## Examples

### Basics

The trigger shows the formatted time; opening it reveals a `TimeInput` for typing plus scrollable columns for browsing. Controllable via `value` / `defaultValue` / `onValueChange`.

```tsx
import { useState } from "react";
import { TimePicker } from "@protocore/pds";

type Time = { hours: number; minutes: number; seconds: number };

export default function Basics() {
  const [value, setValue] = useState<Time | null>({ hours: 9, minutes: 30, seconds: 0 });

  return (
    <div style={{ width: 240 }}>
      <TimePicker aria-label="Booking time" value={value} onValueChange={setValue} locale="en-GB" />
    </div>
  );
}
```

### 12-hour, seconds & step

Set `hour12` for an AM/PM clock, `withSeconds` for a seconds column, and `minuteStep` to thin the minute column to rounded slots (e.g. every 15 minutes).

```tsx
import { useState } from "react";
import { TimePicker } from "@protocore/pds";

type Time = { hours: number; minutes: number; seconds: number };

export default function Variants() {
  const [value, setValue] = useState<Time | null>({ hours: 13, minutes: 15, seconds: 0 });

  return (
    <div style={{ width: 240 }}>
      <TimePicker
        aria-label="Appointment"
        value={value}
        onValueChange={setValue}
        hour12
        minuteStep={15}
        locale="en-US"
      />
    </div>
  );
}
```

## Do & don't

**Do**

- Give the trigger an accessible name via a `Field` label or `aria-label`.
- Set `minuteStep` to the granularity your booking really supports.
- Pass a `locale` so the field value matches the surrounding UI.
- Keep the embedded TimeInput for users who would rather type than scroll.

**Don't**

- Don't use a 1-minute column for a coarse scheduling flow — the list becomes a wall.
- Don't leave the trigger unlabelled when no visible field label wraps it.
- Don't reformat the value in state — read the `TimeValue` from `onValueChange`.
- Don't nest it inside another popover trigger where overlays contend for focus.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Enter / Space` | Open the overlay from the focused trigger. |
| `Arrow keys` | Step the embedded TimeInput's focused segment. |
| `Tab` | Move into the hour / minute columns. |
| `Esc` | Close the overlay without changing the value. |

**Notes**

- The overlay is a Radix Popover: portalled, dismissed on outside-click and Escape, and returns focus to the trigger on close.
- Each column is a `role="listbox"` of `role="option"` buttons with `aria-selected` on the current value.
- `invalid` sets `aria-invalid` on the trigger; the trigger exposes `aria-expanded` for its open state.

## Related

`time-input`, `date-time-picker`, `date-picker`, `field`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
