<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# TimerTray

The timers that are running, paused or finished, in one strip, so a person who leaves a timer's screen can still see it count down, hear it at zero, and stop it. Pass every timer the app has, by the `timerId` its `Timer` uses; the tray shows only those not idle, and hides itself while none are. When a timer reaches zero while only the tray shows it, the tray chimes and says so; when its `Timer` is on the page too, the `Timer` alerts instead (with its own `silent`), whichever of the two mounted first.

```tsx
import { TimerTray } from "@/components/ouro/timer/timer-tray";
```

## Example

```tsx
<TimerTray timers={[{ timerId: "pasta", label: "Pasta", duration: 600, href: "/steps/2" }]} />
```

## Usage

Put it where it stays in view, such as under the header (`className="sticky top-0"`), and leave out the timer on the screen in view.

## Use instead when

- Use `Timer` instead for one timer with all its controls.

## Props

Type: `TimerTrayProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alert` | `"once" \| "repeat"` | `"once"` | How it sounds when a timer reaches zero while only the tray shows it, as `Timer`'s `alert`: `once`, or `repeat`, again every 2.5 seconds until the timer is dismissed or `repeatFor` seconds pass. Dismiss or Cancel in the tray also stops a `Timer`'s repeating alert. |
| `className` | `string` |  | Layout only: margins, sticky placement (`sticky top-0`). Appearance classes warn in development. |
| `finalFocus` | `RefObject<HTMLElement \| null>` |  | Where focus goes when Cancel or Dismiss stops the tray's last timer, such as the page heading (give it `tabIndex={-1}`): `finalFocus={headingRef}`. Stopping any other timer moves focus to the next timer's button, or the previous one's. Without it focus goes to the tray, which stays in view, empty, until focus leaves it. |
| `label` | `string` | `"Timers"` | Names the tray for screen readers, as a region. |
| `labels` | `TimerLabels` |  | The words it shows and says, for apps in other languages. |
| `ref` | `Ref<HTMLElement>` |  | Reaches the rendered element. |
| `repeatFor` | `number` | `120` | With `alert="repeat"`, how long it keeps sounding, in seconds from when the time was up. A value that is not finite and above 0 is taken as 120, and development warns. |
| `silent` | `boolean` |  | No chime and no vibration when a timer reaches zero while only the tray shows it. A `Timer` on the page that shows the same timer alerts instead of the tray, by its own `silent`. |
| `timers` (required) | `readonly TimerTrayItem[]` |  | The timers to watch, by `timerId`. Only those that are running, paused or finished show. |

## Native attributes

Also accepts the attributes in React's `HTMLAttributes` (`id`, `aria-*`, event handlers), passed to the rendered element, except those named on this page.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `hidden`: Not a prop. The tray shows only timers that are running, paused or finished; pass them all.
- `items`: Not a prop. The tray lists `timers`: `<TimerTray timers={[{ timerId, label, duration }]} />`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`). Your classes always win over Ouro UI's: TimerTray merges `className` last with `cn` (tailwind-merge), so a class that conflicts with its own replaces it.[^layout-classes]

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Types

### `TimerDoneEvent`

What a timer's `onDone` hears when it reaches zero.

```ts
interface TimerDoneEvent {
  /**
   * When it reached zero, in milliseconds since the epoch: in the past when the page was closed at the time.
   */
  endedAt: number;
  /**
   * Whether this `useTimer` is the one on the page that should sound the alert: the first with
   * the timer's `timerId` to hear it, not counting `fallback` ones unless only those are mounted. When
   * a `Timer` and a `TimerTray` both show one timer, the `Timer` alerts, whichever mounted first.
   * A hook without `onDone` reads the timer without taking the alert.
   */
  first: boolean;
}
```

### `TimerLabels`

The words a timer shows and says, for apps in other languages.

```ts
interface TimerLabels {
  /** @default "Start" */
  start?: string | undefined;
  /** @default "Pause" */
  pause?: string | undefined;
  /** @default "Resume" */
  resume?: string | undefined;
  /** @default "Cancel" */
  cancel?: string | undefined;
  /** @default "Dismiss" */
  dismiss?: string | undefined;
  /** @default "Restart" */
  restart?: string | undefined;
  /**
   * What it shows, and screen readers hear, at zero. @default "Time's up"
   */
  done?: string | undefined;
  /** What it shows after the time while paused. @default "Paused" */
  paused?: string | undefined;
  /** TimerTray's link to the timer's page. @default "Go to" */
  goTo?: string | undefined;
}
```

### `TimerTrayItem`

One timer a `TimerTray` watches.

```ts
interface TimerTrayItem {
  /** The `timerId` the timer's `Timer` or `useTimer` has. */
  timerId: string;
  /** Not a key. A timer is named by `timerId`, as its `Timer` has it: `{ timerId: "pasta", label, duration }`. */
  id?: { 'Use timerId: "pasta"': never };
  /** What it is for, as its `Timer` says it: "Pasta". */
  label: string;
  /** Its length in seconds, as its `Timer` has it. */
  duration: number;
  /** Where the timer's own screen is, for a "Go to" link. */
  href?: string | undefined;
  /**
   * Called once when this timer finishes. `event.first` says whether this view owns the alert;
   * when a Timer shares it, that Timer owns it instead.
   */
  onDone?: ((event: TimerDoneEvent) => void) | undefined;
}
```

[^layout-classes]: test:registry/ouro/components/ouro/timer/timer-tray.test.tsx::TimerTray > forwards a ref, passes attributes through, and lets layout classes win verify:testExists(registry/ouro/components/ouro/timer/timer-tray.test.tsx::TimerTray > forwards a ref, passes attributes through, and lets layout classes win)
