Timer

A working countdown, interval timer or stopwatch for the task at hand.

Pattern

≈ 709 tokens per use (tool 431 · guidance 237 · typical payload 41)How token costs are estimated

Example: Timer

Set timers for the roast.

Lamb roast

Roast
Roast: 1 hour 20 minutes remaining
  1. Now:Roast1 h 20 min
  2. Up next:Rest20 min

Not started

When to use

  • Timing matters to what the user is doing right now: cooking, steeping, resting dough, a workout, a pomodoro, a rehearsed talk.
  • The user asks the agent to set a timer, run intervals, or time something.

When not to use

  • The user needs a reminder at a clock time, hours or days away. Use a reminder or calendar tool.
  • The time is only information, like "how long do eggs boil?". Answer in text.
  • The agent would need to act when it ends without the user present. The timer runs in the user's browser.

How the agent should use it

  • Give it a short title, the thing being timed: "Roast", "Tea", "Talk". It's used in every control label ("Pause Roast timer") and in the announcement at zero ("Roast is done").
  • Use one step for a countdown, a sequence of labelled steps for intervals, and repeat for rounds instead of listing them.
  • Leave autoStart off unless the user asked to start now. The user presses Start.
  • On completed, move on to the next thing they need. On cancelled, don't restart it unasked.

How it works

ModeShowsControls
countdownTime left, a progress ring, "Ends at 4:12 PM"Start / Pause / Resume, −1 min, +1 min, Reset, Cancel
intervalsTime left in the step, the round, and the step list with the current step highlightedThe same, plus Skip step. ±1 min is off unless allowAdjust
stopwatchElapsed time and the current lapStart / Pause / Resume, Lap, Finish, Reset, Cancel
  • Time comes from the wall clock, not from counting ticks. The display refreshes four times a second and stops refreshing while the tab is hidden; a single timeout still catches the end, so the beep plays in a background tab.
  • At zero the readout says "Done", screen readers hear "Roast is done" once, and a short beep plays if the user started the timer themselves (browsers only allow sound after a gesture). The page title is left alone. Restart runs it again; Dismiss leaves the finished timer in place.
  • ±1 min changes the current step only. It can't take the last minute off: that's what Skip step is for.
  • Laps are listed newest first. The fastest and slowest are labelled in text, not only by color. Split times are read to screen readers only.
  • Reset and Skip step are icon buttons with full accessible names. Running / Paused isn't printed (the main button already says Pause or Resume); screen readers still hear it.
  • Keyboard: Space starts and pauses when focus is on the timer (the readout is focusable). Every control names the timer: "Skip step in Roast timer".

Two kinds of history

A timer runs for minutes or hours, so there are two ways to restore it:

PropBehavior
Saved progressonProgress → progressSave the state on every start, pause, lap and adjustment. On reload the timer picks up where the wall clock says it is, still running if it was. Nothing is sent to the agent
A resultresultThe run finished or was cancelled. It shows the finished timer, read-only
<Timer
  {...part.input}
  progress={savedTimer}
  onProgress={(state) => saveTimer(part.toolCallId, state)}
  result={part.state === "output-available" ? part.output : undefined}
/>

The saved state is small and holds no durations, only when it started and how much ran before:

{ startedAt: 1760000000000, elapsedBeforePause: 0, running: true, adjustSeconds: 0 }

Installation

npx shadcn@latest add https://your-site.com/r/timer.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { timerTool } from "@/components/genui/timer.schema"

const tools = {
  [timerTool.name]: tool({
    description: timerTool.description,
    inputSchema: timerTool.inputSchema,
  }),
}
components/message.tsx
<Timer
  {...part.input}
  status={part.state === "input-streaming" ? "streaming" : undefined}
  result={part.state === "output-available" ? part.output : undefined}
  onResult={(result) =>
    addToolResult({ tool: "start_timer", toolCallId: part.toolCallId, output: result })
  }
/>

The agent receives one result, when the run reaches zero, the user presses Finish on a stopwatch, or the user cancels:

{ action: "completed", totalSeconds: 6000 }
{ action: "completed", totalSeconds: 312.4, laps: [61.2, 94.8, 88, 68.4] } // stopwatch
{ action: "cancelled", elapsedSeconds: 95.3 }

With LangGraph

Push it from a graph node with ui.push({ name: "start_timer", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored result back as result. A completed timer shows "Done" (a stopwatch shows its final time and laps), a cancelled one shows how long it ran, and onResult is never called again.

<Timer {...part.input} result={part.state === "output-available" ? part.output : undefined} />

Schema

PropTypeNotes
titlestring?The thing being timed, up to 40 characters
mode"countdown" | "intervals" | "stopwatch"
steps{ label, seconds }[]?Countdown: one step. Intervals: the sequence. Omit for a stopwatch
repeatnumberIntervals: rounds of the sequence, 1–50. Default 1
autoStartbooleanDefault false: the user starts it
allowAdjustboolean?Shows ±1 min. Default true for a countdown, false otherwise

Host props: status (streaming, ready, completed, cancelled), onResult, onProgress, progress, result and className.

Source

On this page