Componentticket-stub1.25 KB gzipZero dependencies

Stopwatch V2

Digital LED chronometer with laps.

Preview

Live Preview

CHRONO

00:00.00

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add stopwatch-v2

Or install the full npm package:

npm install bigbullui
import { StopwatchV2 } from "@/components/ui/stopwatch-v2";

<StopwatchV2 />

Usage guidance

Use Stopwatch V2 when a ticket stub surface needs Digital LED chronometer with laps. Keep props minimal and prefer composition with sibling stubs.

Avoid Stopwatch V2 for long form flows or dense data grids where a dedicated layout block fits better.

Source code

stopwatch-v2.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface StopwatchV2Props extends React.HTMLAttributes<HTMLDivElement> {
  label?: string;
}

function pad(n: number, len = 2): string {
  return String(n).padStart(len, "0");
}

export function StopwatchV2({ label = "CHRONO", className, ...props }: StopwatchV2Props) {
  const [elapsed, setElapsed] = React.useState(0);
  const [running, setRunning] = React.useState(false);
  const [laps, setLaps] = React.useState<number[]>([]);
  const startRef = React.useRef(0);
  const baseRef = React.useRef(0);

  React.useEffect(() => {
    if (!running) return;
    startRef.current = performance.now();
    let raf = 0;
    const tick = (t: number) => {
      setElapsed(baseRef.current + (t - startRef.current));
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [running ]);

  const toggle = () => {
    if (running) {
      baseRef.current = elapsed;
      setRunning(false);
    } else {
      setRunning(true);
    }
  };

  const reset = () => {
    setRunning(false);
    baseRef.current = 0;
    setElapsed(0);
    setLaps([]);
  };

  const ms = Math.floor(elapsed % 1000);
  const s = Math.floor(elapsed / 1000) % 60;
  const m = Math.floor(elapsed / 60000) % 60;
  const h = Math.floor(elapsed / 3600000);

  return (
    <div className={cn("w-full max-w-xs rounded-lg border-2 border-foreground bg-card p-4 text-center", className)} {...props}>
      <p className="font-mono text-[10px] uppercase tracking-[0.25em] text-muted-foreground">{label}</p>
      <p className="mt-1 font-mono text-4xl font-bold tabular-nums text-accent" aria-live="polite">
        {h > 0 ? `${pad(h)}:` : ""}
        {pad(m)}:{pad(s)}
        <span className="text-lg">.{pad(Math.floor(ms / 10))}</span>
      </p>
      <div className="mt-3 flex justify-center gap-2">
        <button
          type="button"
          onClick={toggle}
          className="cursor-pointer rounded-md bg-accent px-4 py-1.5 font-mono text-xs font-bold uppercase text-accent-foreground transition-transform hover:scale-105 active:scale-95"
        >
          {running ? "Stop" : "Start"}
        </button>
        <button
          type="button"
          onClick={() => setLaps((prev) => [...prev, elapsed])}
          disabled={!running}
          className="cursor-pointer rounded-md border border-border px-4 py-1.5 font-mono text-xs uppercase text-muted-foreground transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
        >
          Lap
        </button>
        <button
          type="button"
          onClick={reset}
          className="cursor-pointer rounded-md border border-border px-4 py-1.5 font-mono text-xs uppercase text-muted-foreground transition-colors hover:text-foreground"
        >
          Reset
        </button>
      </div>
      {laps.length > 0 ? (
        <ol className="mt-3 max-h-28 space-y-1 overflow-y-auto border-t border-dashed border-border pt-2 text-left">
          {laps.map((lap, i) => (
            <li key={i} className="flex justify-between font-mono text-[11px] text-muted-foreground">
              <span>LAP {i + 1}</span>
              <span className="tabular-nums">
                {pad(Math.floor(lap / 60000))}:{pad(Math.floor(lap / 1000) % 60)}.{pad(Math.floor((lap % 1000) / 10))}
              </span>
            </li>
          ))}
        </ol>
      ) : null}
    </div>
  );
}

Props

PropTypeDescription
labelstringHeader label (default 'CHRONO').