- Two modes
- Stopwatch (count up) — animated dial for hours, minutes, seconds.
- Timer (count down) — same dial, but time is set by dragging ring dots.
- Unified circular dial
- Outer ring = hours, middle ring = minutes, inner ring = seconds.
- Drag dots to set time in Timer; rings are read-only in Stopwatch.
- Controls
- Stopwatch: Reset, Play/Pause, Save.
- Timer: Reset, Play/Pause (Play button is centered when there are only two buttons).
- Saved Times (Stopwatch only)
- Save current time; rename inline; delete one or Clear all.
- Deleting is blocked while the stopwatch is running.
- Persisted in
localStorage.
- Audio alert when Timer finishes (
assets/alarm.mp3). - PNG icons for controls (reset/play/pause/flag).
- Accessibility
- Buttons have
aria-labels; rename supportsEnter(save) andEscape(cancel).
- Buttons have
-
React
-
Vite
-
CSS Modules / Tailwind (optional)
-
JavaScript (ES6+)
-
React Hooks (useState, useEffect, useRef)
-
React + Vite
-
Custom hooks
useStopwatch— high‑precision counting with pause/reset.useCountdown— countdown viarequestAnimationFrame+ refs to avoid stale closures.
-
SVG rendering
- One generic
Dialcomponent does angles, dash progress, ticks, and numbers.
- One generic
-
SCSS
- Co-located styles per component (e.g.,
Dial/Dial.scss) + a small sharedstyles/_common.scssfor tokens/card/utility.
- Co-located styles per component (e.g.,
npm inpm run devnpm run build
npm run preview- Dial (generic)
- Props:
size,ticks,showNumbers,rings[],disabled,onRingChange(key, val) - Each ring:
{ key, radius, value, max, baseClass, progClass, dotClass, interactive } - Converts pointer → angle → value; draws base ring, progress (dasharray), and draggable dot.
- Props:
- CircularDial
- Read-only dial used by Stopwatch; passes live
hours/minutes/secondsvalues.
- Read-only dial used by Stopwatch; passes live
- TimeSetterDial
- Interactive dial used by Timer; blocked while counting down; receives current remaining values for live display.
- CircleButton / ControlsRow
- Shared controls UI.
ControlsRowauto-centers the middle button when there are only two children (Timer).
- Shared controls UI.
- Stopwatch
- Uses
useStopwatch; supports save/rename/delete; persists tolocalStorage.
- Uses
- Countdown
- Uses
useCountdown; playsalarm.mp3at the end; start withstart(totalMs); pause/resume viatoggle().
- Uses
- State:
isRunning,elapsedTime - API:
toggle(),reset() - Derived:
timeFormatted,hoursValue,minutesValue,secondsValue
- State:
timeLeft,isRunning,isFinished - Refs:
isRunningRef,endAtRef,lastLeftRef(no stale state in RAF loop) - API:
start(ms),toggle(),reset(),setNewTime(ms) - Derived:
timeFormatted,timeLeftMs,minutes,seconds,centiseconds
- Stored in
localStorageunder thesavedTimeskey. - Each record:
{ id, mode: 'stopwatch', time: 'MM:SS:CS', label }. - Editing: click the label → type → Enter saves, Esc cancels.
- Deleting: single entry (✕) or Clear all.
- While the stopwatch is running, deletion/clear is disabled.
MIT — feel free to use, modify, and distribute. See LICENSE (or change to your preferred license).
