Basic Stopwatch
Stopped
elapsed0.0s
import { defineStates, createMachine, createStoreMachine, addStoreApi, withSubscribe, setup, enter, when, effect,} from "matchina";import { tickEffect } from "../lib/tick-effect";
interface StopwatchState { elapsed: number; at: number;}
const createStopwatchStore = () => { const store = createStoreMachine<StopwatchState>( { elapsed: 0, at: 0 }, { tick: () => (change) => ({ elapsed: change.from.elapsed + (Date.now() - change.from.at), at: Date.now(), }), start: () => (change) => ({ elapsed: change.from.elapsed, at: Date.now(), }), resume: () => (change) => ({ elapsed: change.from.elapsed, at: Date.now(), }), clear: () => () => ({ elapsed: 0, at: Date.now() }), } ); return addStoreApi(withSubscribe(store));};
export const createStopwatchMachine = () => { const states = defineStates({ Stopped: undefined, Ticking: undefined, Suspended: undefined, });
const store = createStopwatchStore();
const machine = createMachine( states, { Stopped: { start: "Ticking" }, Ticking: { _tick: "Ticking", stop: "Stopped", suspend: "Suspended", clear: "Ticking", }, Suspended: { stop: "Stopped", resume: "Ticking", clear: "Suspended", }, }, "Stopped" );
setup(machine)( effect((ev) => { if (ev.type === "_tick") store.api.tick(); if (ev.type === "start") store.api.start(); if (ev.type === "resume") store.api.resume(); if (ev.type === "clear") store.api.clear(); }), enter( when( (ev) => ev.to.is("Ticking"), () => tickEffect(() => machine.send("_tick")) ) ) );
return Object.assign(machine, { store });};import { getAvailableActions } from "matchina";import { useMachine } from "matchina/react";import type { createStopwatchMachine } from "./machine";
type StopwatchMachine = ReturnType<typeof createStopwatchMachine>;
export function StopwatchView({ machine }: { machine: StopwatchMachine }) { useMachine(machine); useMachine(machine.store);
const state = machine.getState(); const elapsed = machine.store.getState().elapsed; const isTicking = state.is("Ticking"); const isStopped = state.is("Stopped");
const dot = isTicking ? "bg-green-600 dark:bg-green-400" : isStopped ? "bg-destructive" : "bg-amber-500 dark:bg-amber-400";
const btnVariant = (event: string) => event === "start" || event === "resume" ? "btn-primary" : event === "suspend" ? "btn-outline" : /* stop, clear */ "btn-ghost";
return ( <div className="flex flex-col items-center gap-6 py-2"> <span className="badge badge-outline gap-1.5"> <span className={`size-1.5 rounded-full ${dot} ${isTicking ? "animate-pulse" : ""}`} /> {state.key} </span>
<div className="flex flex-col items-center gap-1"> <span className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">elapsed</span> <span className="text-6xl font-semibold tabular-nums text-foreground"> {(elapsed / 1000).toFixed(1)} <span className="text-2xl font-normal text-muted-foreground ml-0.5">s</span> </span> </div>
<div className="flex flex-wrap justify-center gap-2"> {getAvailableActions(machine.transitions, state.key) .filter(e => !e.startsWith("_")) .map(event => ( <button key={event} className={`btn ${btnVariant(event)} ${event === "clear" ? "btn-sm" : ""}`} onClick={() => machine.send(event as any)} > {event} </button> ))} </div> </div> );}import { createStopwatchMachine } from "./machine";import { StopwatchView } from "./StopwatchView";
export function Stopwatch() { const machine = createStopwatchMachine(); return <StopwatchView machine={machine} />;}
// Default export for the examplesexport default Stopwatch;