Timer

A stopwatch card whose digits roll on a spinning counter, guarded by a start confirmation.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/timer.json
Usage
Drop it in and listen — the reel, the confirmation popover, the running state and the wall-clock accounting are all internal.
TSImport
import { Timer } from "@/components/blocks/timer";
TSExample
"use client";

import { Timer } from "@/components/blocks/timer";

export function Example() {
  return (
    <Timer
      label="Time tracked"
      confirmDescription="Time starts counting immediately and is logged against this task."
      onEvent={(event) => {
        if (event.type === "stop") console.log("logged", event.seconds);
      }}
    />
  );
}
Composition
Anatomy of the card, the digit reels, and the two action states.
Timer                            (single component — pass everything as props)
└── card-surface <button>        (the whole card is the control; goes red running)
    ├── icon                     (Play when off, Square when running)
    ├── label                    (muted caption, truncates)
    ├── reel                     (transitions-dev spinning counter)
    │   └── t-reel-col[n]        (one clipped window per digit)
    │       └── t-reel-strip     (0-9 cells; rolls forward, blur decays to 0)
    └── off → Popover            (start confirmation)
        │   ├── header           (confirmTitle / confirmDescription)
        │   ├── confirmBody      (any ReactNode)
        │   └── actions          (confirmActions({ start, cancel }) or the
        │                         default Cancel + confirm pair)
        on  → click stops        (no confirmation on the way down)
Default
The whole card is the control. Tapping it while off opens a confirmation popover; once running it turns red and a single tap stops it, no confirmation. Each digit is its own clipped reel that rolls forward and settles out of a vertical-only blur.
Resumed session
`defaultSeconds` picks up where a previous session left off, and the display widens from MM:SS to H:MM:SS past the hour. The confirmation copy is all overridable.
Custom confirmation
Everything in the popover is a prop: `confirmTitle` / `confirmDescription` (either can be `null` to drop that line), `confirmBody` for arbitrary content, and `confirmActions` to replace the button row — it receives `start` and `cancel` so custom buttons still drive the timer.
Event API
`onEvent` emits `start`, `stop`, and one `tick` per whole second — elapsed time is derived from the wall clock, so a throttled background tab can't lose seconds.

onEvent

Start the timer to see events land here.

API Reference
Props for the timer and the shape of an emitted event.

Timer

PropTypeDefaultDescription
labelstring"Time tracked"Muted caption above the digits.
defaultSecondsnumber0Elapsed seconds to resume from.
confirmTitleReactNode | null"Start the timer?"Heading of the start-confirmation popover. `null` drops the header.
confirmDescriptionReactNode | null"Time starts counting immediately…"Body copy of the start-confirmation popover. `null` drops it.
confirmBodyReactNode—Arbitrary content rendered between the popover header and its buttons.
confirmActions(api: TimerConfirmApi) => ReactNode—Replaces the popover's button row. Called with `start` / `cancel`.
confirmLabelstring"Start timer"Label of the default confirm button.
confirmSide"top" | "right" | "bottom" | "left""bottom"Which edge of the card the popover anchors to.
confirmAlign"start" | "center" | "end""start"How the popover aligns along that edge.
confirmSideOffsetnumber4Gap in px between the card and the popover.
onEvent(event: TimerEvent) => void—Fires on start, on stop, and once per whole second while running.
classNamestring—Override the card's width or layout classes.

TimerConfirmApi

PropTypeDefaultDescription
start() => void—Starts the timer and closes the popover.
cancel() => void—Closes the popover without starting.

TimerEvent

PropTypeDefaultDescription
type"start" | "stop" | "tick"—What happened.
secondsnumber—Elapsed whole seconds at the moment of the event.
atnumber—`Date.now()` when the event fired.