# Time Picker

A time field people type into, with a list of times to pick from.

Source: [time-picker.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/time-picker.tsx)

```tsx
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerBasic() {
  return (
    <TimePicker>
      <TimePickerInput aria-label="Start time" />
      <TimePickerContent />
    </TimePicker>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/time-picker
```

## Usage

```tsx
import * as React from "react";

import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export function StartTimePicker() {
  const [time, setTime] = React.useState<string | null>(null);

  return (
    <TimePicker value={time} onValueChange={setTime}>
      <TimePickerInput aria-label="Start time" />
      <TimePickerContent />
    </TimePicker>
  );
}
```

`TimePickerInput` splits the time into an hour, a minute and, on a 12-hour clock, AM or PM. Each
part takes typed digits and moves on once it's filled, so `9`, `3`, `0`, `p` gives 9:30 PM, and `a`
or `p` sets AM or PM from any part. Arrow keys step a part up and down, and Backspace clears it.
Pasting a time such as `14:30` or `2:30 pm` fills every part, and pasting a number such as `45`
fills only the focused one. `TimePickerContent` adds a clock button to the input that opens a list
of times, every 15 minutes by default.

The value is a 24-hour `HH:mm` string, such as `"21:30"`, the format of a native time input.
`onValueChange` gets it once every part is filled in, and `null` once one is cleared.

## Composition

- `TimePicker`
  - `TimePickerInput`
  - `TimePickerContent`

## Examples

### With a label

Put the picker in a `Field` with a `FieldLabel`. The input reads out the field's
`FieldDescription` and `FieldError`, and turns invalid or disabled with the field. Without a visible
label, give the input an `aria-label`.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerWithLabel() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel htmlFor="reminder-time">Daily reminder</FieldLabel>
      <TimePicker defaultValue="08:30">
        <TimePickerInput id="reminder-time" className="w-full" />
        <TimePickerContent />
      </TimePicker>
      <FieldDescription>We'll nudge you at this time every weekday.</FieldDescription>
    </Field>
  );
}
```

### With a date picker

Set a [Date Picker](https://ui.jarv.is/components/date-picker.md) beside it for a date and a time. They're the same
height in each size.

```tsx
import { DatePicker, DatePickerContent, DatePickerTrigger } from "@/components/ui/date-picker";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerDateAndTime() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel htmlFor="meeting-date">Meeting starts</FieldLabel>
      <div className="flex gap-2">
        <DatePicker defaultValue={new Date(2025, 5, 12)}>
          <DatePickerTrigger id="meeting-date" className="min-w-0 flex-1" />
          <DatePickerContent />
        </DatePicker>
        <TimePicker defaultValue="14:00">
          <TimePickerInput aria-label="Start time" />
          <TimePickerContent />
        </TimePicker>
      </div>
      <FieldDescription>Guests see it in their own time zone.</FieldDescription>
    </Field>
  );
}
```

To combine their values, set the time on a copy of the date:

```tsx
function combine(date: Date, time: string) {
  const [hours, minutes] = time.split(":").map(Number);
  const dateTime = new Date(date);
  dateTime.setHours(hours, minutes, 0, 0);
  return dateTime;
}
```

### Hour cycle

The input follows the hour cycle of its `locale`, a DayPicker locale like the Date Picker's, which
is 12 hours for the default English. `hourCycle={24}` runs hours from 0 to 23 instead, with no AM or
PM. The list writes times the locale's way, and the input puts AM or PM first in locales that do,
such as Chinese.

```tsx
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerHourCycle() {
  return (
    <TimePicker hourCycle={24} defaultValue="18:45">
      <TimePickerInput aria-label="Departure time" />
      <TimePickerContent />
    </TimePicker>
  );
}
```

### Earliest and latest times

`min` and `max` bound the list, and the input turns invalid outside them. `step` sets the minutes
between the times in the list. A `min` later than `max` makes a range that runs past midnight, so
`min="22:00" max="02:00"` offers the times from 10:00 PM to 2:00 AM.

```tsx
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerRange() {
  return (
    <Field className="w-48">
      <FieldLabel htmlFor="pickup-time">Pickup time</FieldLabel>
      <TimePicker min="09:00" max="17:00" step={30}>
        <TimePickerInput id="pickup-time" className="w-full" />
        <TimePickerContent />
      </TimePicker>
      <FieldDescription>Open 9:00 AM to 5:00 PM.</FieldDescription>
    </Field>
  );
}
```

### Seconds

`granularity="second"` adds seconds, and the value becomes `HH:mm:ss`. Leave out
`TimePickerContent` for an input on its own.

```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { TimePicker, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerSeconds() {
  return (
    <Field className="w-48">
      <FieldLabel htmlFor="clip-start">Clip starts at</FieldLabel>
      <TimePicker granularity="second" hourCycle={24} defaultValue="00:01:30">
        <TimePickerInput id="clip-start" />
      </TimePicker>
    </Field>
  );
}
```

### In a form

Give a `Field` or the picker a `name`, and the value submits under it as `HH:mm`, or pass `form`
with a form's `id` when the picker sits outside it. With `required`, the form won't submit without a
time, which fails as `valueMissing`. A time outside `min` and `max`, or one only partly typed, stops
it too, as `customError`, so a `FieldError` matching it shows the reason. A form reset brings back
`defaultValue`.

```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";
import { toast } from "@/components/ui/toast";

export default function TimePickerForm() {
  return (
    <Form
      className="max-w-sm"
      onFormSubmit={(values) =>
        toast.add({ title: "Pickup booked", description: `See you at ${values.pickup}.` })
      }
    >
      <Field name="pickup">
        <FieldLabel>Pickup time</FieldLabel>
        <TimePicker required min="09:00" max="17:00" step={30}>
          <TimePickerInput className="w-full" />
          <TimePickerContent />
        </TimePicker>
        <FieldDescription>Open 9:00 AM to 5:00 PM.</FieldDescription>
        <FieldError match="valueMissing">Choose a pickup time.</FieldError>
        <FieldError match="customError" />
      </Field>
      <Button type="submit" variant="primary" className="self-start">
        Book pickup
      </Button>
    </Form>
  );
}
```

### Sizes

`size` on the input matches the sizes of buttons, inputs and selects.

```tsx
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

const sizes = ["sm", "default", "lg"] as const;

export default function TimePickerSizes() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {sizes.map((size) => (
        <TimePicker key={size} defaultValue="09:30">
          <TimePickerInput size={size} aria-label={`Time, ${size}`} />
          <TimePickerContent />
        </TimePicker>
      ))}
    </div>
  );
}
```

### Invalid

Set `invalid` on the `Field`, and explain the problem with a `FieldError`.

```tsx
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerInvalid() {
  return (
    <Field invalid className="w-48">
      <FieldLabel htmlFor="end-time">End time</FieldLabel>
      <TimePicker defaultValue="08:00">
        <TimePickerInput id="end-time" className="w-full" />
        <TimePickerContent />
      </TimePicker>
      <FieldError>End the event after it starts, at 9:00 AM.</FieldError>
    </Field>
  );
}
```

### Disabled

```tsx
import { TimePicker, TimePickerContent, TimePickerInput } from "@/components/ui/time-picker";

export default function TimePickerDisabled() {
  return (
    <TimePicker disabled defaultValue="12:00">
      <TimePickerInput aria-label="Lunch time" />
      <TimePickerContent />
    </TimePicker>
  );
}
```

## Props

`TimePicker` takes the props of [Popover](https://ui.jarv.is/components/popover.md)'s root too, such as `open` and
`onOpenChange` for the list.

| Part | Extends |
| --- | --- |
| `TimePicker` | [`Popover.Root`](https://base-ui.com/react/components/popover#root) |
| `TimePickerInput` | `<div>` |
| `TimePickerContent` | [`PopoverContent`](https://ui.jarv.is/components/popover.md) |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### TimePicker

| Prop                    | Type                                                                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ----------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `defaultOpen?`          | `boolean`                                                                  | Whether the popover is initially open. To render a controlled popover, use the `open` prop instead. Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `open?`                 | `boolean`                                                                  | Whether the popover is currently open.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `onOpenChange?`         | `((open: boolean, eventDetails: Popover.Root.ChangeEventDetails) => void)` | Event handler called when the popover is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `onOpenChangeComplete?` | `((open: boolean) => void)`                                                | Event handler called after any animations complete when the popover is opened or closed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `actionsRef?`           | `React.RefObject<PopoverRootActions \| null>`                              | A ref to imperative actions. - `unmount`: Ends the closing phase of the popover after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the popover completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the popover imperatively when called.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `modal?`                | `"trap-focus" \| boolean`                                                  | Determines if the popover enters a modal state when open. - `true`: user interaction is limited to the popover: document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the popover, but document page scroll is not locked and pointer interactions outside of it remain enabled. On touch devices, a `true` modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior. When `modal` is `true`, focus trapping is enabled only if `<Popover.Close>` is rendered inside `<Popover.Popup>`. It can be visually hidden with your own CSS if needed, such as Tailwind's `sr-only` utility. When `modal` is `'trap-focus'`, render `<Popover.Close>` inside `<Popover.Popup>` so touch screen readers can escape the popup. Default: `false` |
| `triggerId?`            | `string \| null`                                                           | ID of the trigger that the popover is associated with. This is useful in conjunction with the `open` prop to create a controlled popover. There's no need to specify this prop when the popover is uncontrolled (that is, when the `open` prop is not set).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `defaultTriggerId?`     | `string \| null`                                                           | ID of the trigger that the popover is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open popover.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `handle?`               | `Popover.Handle<unknown>`                                                  | A handle to associate the popover with a trigger. If specified, allows external triggers to control the popover's open state.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `children?`             | `PayloadChildRenderFunction<unknown> \| React.ReactNode`                   | The content of the popover. This can be a regular React node or a render function that receives the `payload` of the active trigger.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `value?`                | `string \| null`                                                           | The time as 24-hour `HH:mm`, or `HH:mm:ss` with `granularity="second"`. `null` when there's none.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `defaultValue?`         | `string \| null`                                                           | Default: `null`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `onValueChange?`        | `((value: string \| null) => void)`                                        | Called once every part of the time is filled in, and with `null` once one is cleared.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `hourCycle?`            | `12 \| 24`                                                                 | Whether hours run from 1 to 12 with AM and PM, or from 0 to 23. Default: `The locale's`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `granularity?`          | `"minute" \| "second"`                                                     | The smallest part of the time people set. Default: `"minute"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `step?`                 | `number`                                                                   | Minutes between the times the list offers. Default: `15`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `min?`                  | `string`                                                                   | The earliest time, as `HH:mm`. The list starts here, and earlier times are invalid. A `min` after `max` makes a range that runs past midnight.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `max?`                  | `string`                                                                   | The latest time, as `HH:mm`. The list ends here, and later times are invalid.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `locale?`               | `Partial<DayPickerLocale>`                                                 | A DayPicker locale, for the hour cycle, the AM and PM labels and the times in the list.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `name?`                 | `string`                                                                   | Submits the value with a form under this name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `form?`                 | `string`                                                                   | The `id` of the form to submit the value with, when the picker isn't inside it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `required?`             | `boolean`                                                                  | Whether a form can't be submitted without a time. A time outside `min` and `max` stops it too. Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `disabled?`             | `boolean`                                                                  | Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `readOnly?`             | `boolean`                                                                  | Default: `false`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

### TimePickerInput

| Prop            | Type                        | Description                                                                                                     |
| --------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `size?`         | `"default" \| "lg" \| "sm"` | Default: `"default"`                                                                                            |
| `triggerLabel?` | `string`                    | The clock button's accessible name, when there's a `TimePickerContent` for it to open. Default: `"Choose time"` |

### TimePickerContent

| Prop           | Type                                                                       | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| -------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `side?`        | `"bottom" \| "inline-end" \| "inline-start" \| "left" \| "right" \| "top"` | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Default: `"bottom"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `sideOffset?`  | `number \| OffsetFunction`                                                 | Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. Default: `4` |
| `align?`       | `"center" \| "end" \| "start"`                                             | How to align the popup relative to the specified side. Default: `"start"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `alignOffset?` | `number \| OffsetFunction`                                                 | Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment. The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. Default: `0`  |
| `aria-label?`  | `string`                                                                   | The list's accessible name. Default: `"Times"`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
