Components
Timer
A countdown readout: big tabular m:ss, an optional label, and a bar draining toward zero. Self-ticking (end-time anchored, no drift) with pause/resume via running; pass remaining to control it yourself.
Installation
npx @glasskit-ui/cli add timerInstall the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:
npm install @glasskit-ui/react// components/lib/utils.tsexport type ClassValue = string | number | null | undefined | false;/** * Join truthy class names. Dependency-free on purpose: the lens components * style via bespoke semantic classes (no conflicting Tailwind utilities to * de-dupe), so this needs no clsx/tailwind-merge and resolves from anywhere * the registry is vendored. */export function cn(...inputs: ClassValue[]): string { return inputs.filter(Boolean).join(" ");}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined { return typeof label === "string" ? label : undefined;}// components/glasskit/progress.tsximport type { ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";/** * <Progress> — emitted progress, two shapes: * - "linear" a continuous bar (also covers countdown: feed a decreasing * value and a time label). Uses a native <progress>, so the * dynamic fill needs no inline style. * - "step" discrete step-of-N dots (wizard / pinch-advance). * * `value` is clamped to [0, max]. For "step", value = completed steps. */export function Progress({ value, max = 100, variant = "linear", label, className,}: { value: number; max?: number; variant?: "linear" | "step"; /** Optional caption shown with the bar (e.g. "3 of 5", "0:42 left"). */ label?: ReactNode; className?: string;}) { const clamped = Math.max(0, Math.min(value, max)); if (variant === "step") { // Steps must be a sane integer count — a fractional or negative max // would make Array.from misrender the dots. const steps = Math.max(0, Math.floor(max)); return ( <div className={cn("gk-steps", className)} role="progressbar" aria-valuenow={clamped} aria-valuemin={0} aria-valuemax={steps} aria-label={stringLabel(label)} > {Array.from({ length: steps }, (_, i) => ( <span key={i} className={cn("gk-step", i < clamped && "gk-step--on")} /> ))} </div> ); } return ( <div className={cn("gk-progress", className)}> <progress className="gk-progress__el" value={clamped} max={max} aria-label={stringLabel(label)} /> {label != null ? ( <div className="gk-progress__meta t-caption">{label}</div> ) : null} </div> );}// components/glasskit/timer.tsx"use client";import { useEffect, useRef, useState, type ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";import { Progress } from "./progress";/** * <Timer> — a countdown readout: big tabular m:ss (h:mm:ss past an hour), an * optional label, and a bar draining toward zero. Self-ticking by default: * give it `duration` and flip `running` to pause/resume — ticks are end-time * anchored (no setTimeout drift) and aligned to the second boundary. * `onComplete` fires once at zero. Pass `remaining` to control it instead: * the prop always wins, the component never ticks, and `onComplete` is yours * to call — you own the clock. */export function Timer({ duration, remaining, running = true, label, showBar = true, onComplete, className,}: { /** Total seconds. Drives self-ticking and the drain bar's scale. */ duration?: number; /** Controlled seconds left. Omit to self-tick from `duration`. */ remaining?: number; /** Pause/resume the self-ticking countdown. */ running?: boolean; label?: ReactNode; /** Hide the drain bar (it needs `duration` for its scale). */ showBar?: boolean; /** Fires once when a self-ticking countdown reaches zero. */ onComplete?: () => void; className?: string;}) { const controlled = remaining != null; const [internal, setInternal] = useState(duration ?? 0); // Latest-values refs: the anchor effect reads these at (re)start time so // pausing/resuming re-anchors from the frozen value, and onComplete stays // current without being an effect dependency. const internalRef = useRef(internal); internalRef.current = internal; const onCompleteRef = useRef(onComplete); onCompleteRef.current = onComplete; // A new duration is a new countdown — reset. Runs before the anchor effect // below (declaration order), so the ref is fresh when it re-anchors. useEffect(() => { setInternal(duration ?? 0); internalRef.current = duration ?? 0; }, [duration]); useEffect(() => { if (controlled || !running || internalRef.current <= 0) return; // Anchor to a wall-clock end time: each tick recomputes from it, so // timeout jitter never accumulates into a slow timer. const endAt = Date.now() + internalRef.current * 1000; let timer: ReturnType<typeof setTimeout>; const schedule = () => { timer = setTimeout(tick, ((endAt - Date.now() - 20) % 1000) + 20); }; const tick = () => { const left = Math.max(0, Math.ceil((endAt - Date.now()) / 1000)); setInternal(left); if (left <= 0) { onCompleteRef.current?.(); return; } schedule(); }; schedule(); return () => clearTimeout(timer); }, [controlled, running, duration]); const shown = controlled ? Math.max(0, remaining) : internal; return ( <div className={cn("gk-timer", className)} role="timer" aria-label={stringLabel(label) ?? "Timer"} > <span className="gk-timer__time">{formatSeconds(shown)}</span> {label != null ? ( <span className="gk-timer__label t-caption">{label}</span> ) : null} {showBar && duration != null && duration > 0 ? ( // Decorative: the big time above carries the value (role="timer"), // a visible Progress label here would just repeat the caption. <div aria-hidden="true" className="gk-timer__bar"> <Progress value={shown} max={duration} /> </div> ) : null} </div> );}function formatSeconds(total: number): string { const s = Math.max(0, Math.floor(total)); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const mm = h > 0 ? String(m).padStart(2, "0") : String(m); return `${h > 0 ? `${h}:` : ""}${mm}:${String(s % 60).padStart(2, "0")}`;}Usage
// self-ticking 5-minute countdown<Timer duration={300} label="Pasta" onComplete={notify} />// paused / resumed from app state<Timer duration={300} running={running} />// or controlled — you own the clock<Timer remaining={secondsLeft} duration={300} />Props
Prop
Type