Speed Run Timer
A compact mm:ss stopwatch chip for speed-run and timed-exercise UIs. Counts up from zero, ticks every ~250 ms, surfaces the elapsed time via onTick, and tints amber → red as the optional warnAtMs cap is approached and crossed.
Preview
Installation
npx shadcn@latest add https://craftbits.dev/r/speed-run-timer.jsonUsage
Drive the running state from a parent flag and bump runKey to reset:
import { SpeedRunTimer } from "@craft-bits/core";
import { useState } from "react";
function Practice() {
const [running, setRunning] = useState(false);
const [runKey, setRunKey] = useState(0);
return (
<div className="flex items-center gap-2">
<SpeedRunTimer
running={running}
runKey={runKey}
warnAtMs={90_000}
onTick={(ms) => {
if (ms >= 90_000) setRunning(false);
}}
/>
<button onClick={() => setRunning((r) => !r)}>
{running ? "Stop" : "Start"}
</button>
<button
onClick={() => {
setRunning(false);
setRunKey((k) => k + 1);
}}
>
Reset
</button>
</div>
);
}Uncontrolled (the chip just renders, parent never toggles it):
<SpeedRunTimer defaultRunning />Anatomy
- Chip — a single inline-flex
<div role="timer">rendering themm:ssstring in tabular-nums. - Tone —
normal(warm warning tint),urgent(red, last 60 s beforewarnAtMs),critical(red + pulse, last 10 s). - Reset — change
runKeyto wipe elapsed back to zero without changingrunning.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
running | boolean | — | Controlled running flag. Pair with onRunningChange. |
defaultRunning | boolean | false | Uncontrolled initial running flag. |
onRunningChange | (running: boolean) => void | — | Called when the running state changes (e.g. tab auto-pause). |
runKey | string | number | — | Bumping this value resets the elapsed counter to zero. |
onTick | (msElapsed: number) => void | — | Fires every ~250 ms while running, with the new elapsed time. |
warnAtMs | number | — | Soft cap. Drives the urgent (≤60 s left) and critical (≤10 s) tints. |
format | (msElapsed: number) => string | mm:ss | Override the display string. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- Renders as
<div role="timer">witharia-label="Elapsed time: mm:ss"so screen readers can read it on demand without interrupting on every tick (aria-live="off"). - Auto-pauses when the page becomes hidden (
document.visibilitychange) and auto-resumes when it returns — users who context-switch don't get false-positive time-outs. - The colored tint is reinforced by an
animate-pulseon critical, so urgency is not signaled by color alone.
Credits
- Extracted from:
algoflashcards(src/platform/ui/SpeedRunTimer.tsx). The source was a countdown timer hard-wired to astartedAt/timeLimitMspair plus the project'suseTimershook and thelucide-react<Timer/>icon. craft-bits inverts it to a stopwatch (counts up, no built-in deadline), drops the icon to keep the component dependency-free, replacesuseTimerswith a drift-freesetTimeoutloop, adds tab-hide auto-pause, and exposesrunning/runKey/onTickso the consumer drives start / stop / reset and decides what counts as "time up".