# Dot timer

> A dark recording pill whose round lamp of LEDs sweeps a band of light while the time counts up.

A recording timer in a dark pill. On its left, a round lamp of 21 LEDs rolls a leaning band of light across itself, each LED glowing warm just before it lights and just after; on its right, the time counts up. Press the pill to start and pause, hold it to reset, and pick the LEDs’ colour.

A React component by Bruno Delorence: https://brunodelorence.com/components/dot-timer. MIT License, Copyright (c) 2026 Bruno Delorence.

Category: Feedback.

## Anatomy

1. **Stage**: A soft lilac backdrop, deep violet in the dark, that sets the pill’s size: the pill keeps about 40% of its width.
2. **Pill**: One real button, dark in both themes. Press, Space or Enter starts and pauses; hold 600 ms, or press R or Escape, to reset.
3. **Lamp**: A 5 by 5 grid of LEDs without its corners. While running, a leaning band of light sweeps across it every 1.2 s; stopped, it dims and holds still.
4. **LED**: One of 21 dots, each lit 16 of the sweep’s 36 frames and warm for 3 either side. It takes the `color` prop.
5. **Time**: The time counted up, `m:ss`, and `h:mm:ss` past an hour. Screen readers hear it in words: “1 minute 23 seconds”.
6. **Digit**: Each figure has a fixed width of 0.64 em, so the pill never shifts as the seconds change.
7. **Colon**: A narrower slot, half a digit wide, that keeps the minutes and seconds close.

## When to use

- Showing that something is being recorded, and for how long: a voice memo, a screen capture, a call.
- An elapsed time the person starts and pauses themselves, with one press and nothing else to learn.
- A status that must read from across the room: the sweeping lamp says “live” before the figures are read.
- A time kept by your own recorder: pass `elapsed` and `running` and the pill only shows them.

## When not to use

- Counting down to a deadline. It only counts up; use a countdown that says what happens at zero.
- Times that need tenths or hundredths, like a stopwatch for laps. It shows whole seconds only.
- Several timers side by side: a row of sweeping lamps competes for the eye. Use plain figures and one dot.
- Dense toolbars and table cells: the pill sits on its own stage and wants room around it.

## Installation

Install it with the shadcn CLI, into components/dot-timer of any React project:

```sh
npx shadcn@latest add https://brunodelorence.com/r/dot-timer.json
```

Or copy all the source files below into one folder of your project. Either way it needs React 19 and a bundler with CSS Modules (Next.js, Vite).

It has a dark colour scheme. Its colours are CSS light-dark() pairs, kept inside @supports so that builds which rewrite light-dark() for older browsers leave them alone: it follows your page’s color-scheme (light unless the page sets one) or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

## Usage

```tsx
import { useState } from 'react';
import { DotTimer } from './dot-timer';

export function RecordButton() {
  const [recording, setRecording] = useState(false);
  return (
    <DotTimer
      running={recording}
      onToggle={(running) => setRecording(running)}
      onReset={() => setRecording(false)}
      color="red"
      autoplay={false}
    />
  );
}
```

## States

- **Idle**: At 0:00 and stopped. The lamp is dimmed and still, waiting for the first press.
- **Running**: The lamp sweeps at full strength. Shown here with `elapsed` fixed, so the figures hold; a real one counts.
- **Paused**: The time stays where it stopped and the lamp dims and freezes on its current frame.
- **Past an hour**: The figures become `h:mm:ss` and the pill grows to fit them.
- **Another colour**: With `color="green"` the LEDs glow green; any CSS colour works.

## How it works

- The lamp is a 5 by 5 grid without its corners: 21 LEDs, 5.4 px across and 7.82 px apart. A band of light rolls across it from left to right, leaning so that its bottom leads, once every 1.2 s.
- Each LED runs the same cycle of 36 frames: lit for 16, warm for 3, dim for 14, warm for 3 again. Only where it starts differs: about 5 frames after its left neighbour and 2 before the one above it. That is the whole pattern, a table of 21 numbers.
- The cycle is one CSS animation on opacity, in steps, and each LED gets its starting frame as a negative animation delay. Starting and pausing the timer only plays or pauses the animations; nothing runs per frame in script.
- The clock is timestamps, not ticks: what was counted before the last start, plus the time since. The display re-renders once on each whole second, and not at all off screen or in a hidden tab, where the LEDs pause too; it is right again the moment it is seen.
- Press the pill, or Space or Enter on it, to start and pause; hold it for 600 ms, pressing in as it goes, or press R, Escape, Backspace or Delete, to reset to 0:00. A reset restarts the lamp from its first frame.
- While stopped or paused the lamp holds its frame and dims; the time keeps tabular digits, so the pill keeps its width as they change.
- Everything is drawn in --u, a px of the pill at its own size; the stage sets it from its own width with a container query, so the pill stays about 40% of the stage and never smaller than itself.
- While on screen and untouched, it counts from 0:00 to 0:05 and starts over. The first press or key hands it to the reader for good, at the time it shows.
- Under reduced motion the LEDs hold a half-lit frame as plain opacities, with no animation at all; the time still counts.
- The stage’s colours are light-dark() tokens on the root, lilac by day and deep violet at night; the pill is dark on both. The color prop sets the LEDs: red, amber, green, blue or white, or any CSS colour.

## API reference

- **color** (`'red' | 'amber' | 'green' | 'blue' | 'white' | string`, default `'red'`): The LEDs’ colour: one of the presets, or any CSS colour.
- **running** (`boolean`): Whether it runs, for a controlled timer: it counts while true, and calls onToggle when pressed.
- **defaultRunning** (`boolean`, default `false`): Whether it starts running, for an uncontrolled timer. The demo is then off.
- **elapsed** (`number`): The time shown, in ms, for a controlled time: it shows this and counts nothing itself (the lamp still follows running).
- **defaultElapsed** (`number`, default `0`): The time it counts on from, in ms, for a timer that counts itself.
- **onToggle** (`(running: boolean, elapsed: number) => void`): Called when the reader starts or pauses it, with the new state and the time in ms.
- **onReset** (`(elapsed: number) => void`): Called when the reader resets it, with the time in ms it had reached.
- **label** (`string`, default `'Recording timer'`): The pill’s accessible name; the time, in words, follows it.
- **autoplay** (`boolean`, default `true`): While on screen and untouched, it counts from 0:00 to 0:05 and starts over. Off when it is controlled.
- **colorScheme** (`'light' | 'dark' | 'system'`): Light or dark colours, or the reader’s; without it, the page’s color-scheme.
- **className** (`string`): Classes for the stage.

## Accessibility

- The pill is a real button with aria-pressed: pressed while the timer runs. Its name is the label and the time in words, such as “Recording timer 1 minute 5 seconds”, so it is read on focus without announcing every second.
- Space or Enter starts and pauses; R, Escape, Backspace or Delete resets. A hint, read as the button’s description, says so, and that holding the pill resets it too.
- A polite live region says what changed: started or paused at what time, or reset.
- The lamp and the visible digits are aria-hidden: the button’s name carries the time.
- A visible focus ring outlines the pill; a long press does not open the context menu on touch.
- Under reduced motion the LEDs hold still on a half-lit frame and the time still counts.

## Source

### dot-timer/dot-timer.tsx

```tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.

'use client';

import {
  useEffect,
  useEffectEvent,
  useId,
  useRef,
  useState,
  type CSSProperties,
  type KeyboardEvent as ReactKeyboardEvent,
  type PointerEvent as ReactPointerEvent,
} from 'react';
import { useAutoplay } from './use-autoplay';
import styles from './dot-timer.module.css';

/*
 * Dot timer: a dark recording pill. On its left a round dot-matrix lamp of 21 LEDs, a 5 by 5
 * grid without its corners, sweeps a band of light across itself; on its right the time counts
 * up, m:ss (h:mm:ss past an hour).
 *
 * - The lamp is a lit band that rolls from left to right, leaning so that the bottom leads: every
 *   LED is lit for 16 of the cycle's 36 frames (1.2 s at 30 frames a second), glows warm for 3
 *   frames before and after, and rests dim the rest of the time. Each LED starts its cycle at its
 *   own frame, about 5 frames after its left neighbour and 2 before the one above it (ONSET).
 * - The whole lamp is one CSS animation on the LEDs' opacity, in steps, each LED with its own
 *   negative delay; the timer only pauses or plays it. Nothing redraws per frame in script.
 * - The clock is timestamps, not ticks: the time shown is what had been counted before the last
 *   start plus the time since. The display only re-renders on the next whole second, and not at
 *   all while off screen or in a hidden tab; it is right again the moment it is seen.
 * - Press the pill, or Space or Enter on it, to start and pause; hold it (600 ms), or press R,
 *   Escape, Backspace or Delete, to reset to 0:00.
 * - While on screen and untouched, it counts on its own from 0:00 to 0:05 and starts over, like a
 *   loop. The first real input hands it to the reader for good, at the time it shows.
 * - Reduced motion: the LEDs hold still on a half-lit frame, set as plain opacities (no animation
 *   at all, even paused); the time still counts.
 * - Light or dark: the stage's colours are light-dark() tokens; the pill is dark on both.
 */

/** The LEDs' colour presets; any CSS colour works too. */
export const DOT_TIMER_COLORS = {
  red: '#ff0418',
  amber: '#ffa31a',
  green: '#35f26b',
  blue: '#3aa7ff',
  white: '#f3efe6',
} as const;
export type DotTimerColor = keyof typeof DOT_TIMER_COLORS;

/** Frames in one sweep of the lamp; the CSS animation runs them in 1.2 s. */
const FRAMES = 36;
/** The frame each LED lights on, by row and column; null for the grid's empty corners. */
const ONSET: (number | null)[][] = [
  [null, 10, 15, 20, null],
  [3, 8, 13, 19, 24],
  [1, 7, 12, 17, 22],
  [0, 5, 10, 15, 20],
  [null, 3, 8, 13, null],
];
/** The frame a still lamp holds under reduced motion: its left half lit. */
const HELD = 13;
/** How long a press must be held to reset. */
const HOLD = 600;
/** The demo counts to this and starts over. */
const LOOP = 6000;

const LEDS = ONSET.flatMap((row, y) =>
  row.flatMap((onset, x) => (onset === null ? [] : [{ x, y, onset }])),
);

/** An LED's light, `frame` frames after it lit: lit, warm either side, dim the rest. */
function level(frame: number) {
  return frame < 16 ? 'lit' : frame < 19 || frame >= 33 ? 'warm' : 'dim';
}

/** m:ss, or h:mm:ss from an hour on. */
function format(ms: number) {
  const total = Math.max(0, Math.floor(ms / 1000));
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  const ss = String(s).padStart(2, '0');
  return h ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;
}

/** The time in words, for screen readers: "1 minute 5 seconds". */
function spoken(ms: number) {
  const total = Math.max(0, Math.floor(ms / 1000));
  const parts: [number, string][] = [
    [Math.floor(total / 3600), 'hour'],
    [Math.floor((total % 3600) / 60), 'minute'],
    [total % 60, 'second'],
  ];
  const words = parts.filter(([n]) => n).map(([n, unit]) => `${n} ${unit}${n === 1 ? '' : 's'}`);
  return words.length ? words.join(' ') : '0 seconds';
}

export function DotTimer({
  color = 'red',
  elapsed,
  defaultElapsed = 0,
  running,
  defaultRunning = false,
  onToggle,
  onReset,
  label = 'Recording timer',
  autoplay = true,
  colorScheme,
  className = '',
}: {
  /** The LEDs' colour: a preset or any CSS colour. */
  color?: DotTimerColor | (string & {});
  /** The time shown, in ms, for a controlled timer: it then shows this and counts nothing itself. */
  elapsed?: number;
  /** The time it starts from, in ms, for a timer that counts itself. */
  defaultElapsed?: number;
  /** Whether it is running, for a controlled timer. */
  running?: boolean;
  /** Whether it starts running, for an uncontrolled timer. The demo is then off. */
  defaultRunning?: boolean;
  /** Called when the reader starts or pauses it, with the new state and the time in ms. */
  onToggle?: (running: boolean, elapsed: number) => void;
  /** Called when the reader resets it, with the time it had reached in ms. */
  onReset?: (elapsed: number) => void;
  /** The pill's accessible name; the time follows it. */
  label?: string;
  /** While on screen and untouched, it counts from 0:00 to 0:05 and starts over. */
  autoplay?: boolean;
  /** Light or dark colours, or the reader's ('system'); without it, the page's color-scheme. */
  colorScheme?: 'light' | 'dark' | 'system';
  className?: string;
}) {
  const { ref: root, visible, reduced, taken, take } = useAutoplay<HTMLDivElement>(Infinity);
  const id = useId();

  const runControlled = running !== undefined;
  const timeControlled = elapsed !== undefined;
  const [ownRunning, setOwnRunning] = useState(defaultRunning);
  /** The time counted before the last start, and when that start was (null while stopped). */
  const base = useRef(defaultElapsed);
  const since = useRef<number | null>(null);
  /** Re-renders the display; bumped on each whole second and on every change. */
  const [, setTick] = useState(0);
  const rerender = () => setTick((n) => n + 1);
  /** Restarts the lamp's animation from its first frame, on a reset. */
  const [cycle, setCycle] = useState(0);
  const [message, setMessage] = useState('');
  const holdTimer = useRef(0);
  const held = useRef(false);
  const [holding, setHolding] = useState(false);

  const demo = autoplay && !taken && !runControlled && !timeControlled && !defaultRunning;
  const isRunning = runControlled ? running : demo ? visible : ownRunning;

  const now = () => performance.now();
  const read = () =>
    timeControlled ? elapsed : base.current + (since.current === null ? 0 : now() - since.current);

  /** Keeps the clock's timestamps in step with whether it runs. */
  useEffect(() => {
    if (isRunning && since.current === null) since.current = performance.now();
    if (!isRunning && since.current !== null) {
      base.current += performance.now() - since.current;
      since.current = null;
    }
  }, [isRunning]);

  /** Re-renders on the next whole second while running and seen; the demo loops at LOOP. */
  const step = useEffectEvent(() => {
    if (demo && read() >= LOOP) {
      base.current = 0;
      since.current = now();
    }
    rerender();
  });
  useEffect(() => {
    if (!isRunning || !visible || timeControlled) return;
    let timer = 0;
    const next = () => {
      const t = base.current + (since.current === null ? 0 : performance.now() - since.current);
      timer = window.setTimeout(
        () => {
          step();
          next();
        },
        1000 - (t % 1000) + 8,
      );
    };
    step();
    next();
    return () => window.clearTimeout(timer);
  }, [isRunning, visible, timeControlled]);

  /** Repeats are made to differ, so screen readers announce them again. */
  const announce = (text: string) => setMessage((was) => (was === text ? `${text} ` : text));

  const toggle = () => {
    take();
    const next = !isRunning;
    const at = read();
    if (!runControlled) {
      // The demo's state becomes the reader's: a press while it counts pauses it.
      setOwnRunning(next);
    }
    onToggle?.(next, at);
    announce(next ? `Started at ${spoken(at)}` : `Paused at ${spoken(at)}`);
  };

  const reset = () => {
    take();
    const at = read();
    if (!timeControlled) {
      base.current = 0;
      // A controlled timer that runs keeps running, from 0:00; one of its own stops.
      since.current = runControlled && isRunning ? now() : null;
    }
    if (!runControlled) setOwnRunning(false);
    setCycle((n) => n + 1);
    rerender();
    onReset?.(at);
    announce('Reset to 0 seconds');
  };

  const cancelHold = () => {
    window.clearTimeout(holdTimer.current);
    setHolding(false);
  };
  useEffect(() => () => window.clearTimeout(holdTimer.current), []);

  const onPointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {
    if (event.button !== 0) return;
    held.current = false;
    setHolding(true);
    window.clearTimeout(holdTimer.current);
    holdTimer.current = window.setTimeout(() => {
      held.current = true;
      setHolding(false);
      reset();
    }, HOLD);
  };

  const onClick = () => {
    cancelHold();
    // A held press has reset already; its click does nothing more.
    if (held.current) {
      held.current = false;
      return;
    }
    toggle();
  };

  const onKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
    if (event.metaKey || event.ctrlKey || event.altKey) return;
    if (['r', 'R', 'Escape', 'Backspace', 'Delete'].includes(event.key)) {
      event.preventDefault();
      reset();
    }
  };

  const ms = read();
  const time = format(ms);
  const animate = isRunning && visible && !reduced;
  const hold = reduced ? HELD : 0;
  const led = color in DOT_TIMER_COLORS ? DOT_TIMER_COLORS[color as DotTimerColor] : color;
  const state = isRunning ? 'running' : ms >= 1000 ? 'paused' : 'idle';

  return (
    <div
      ref={root}
      className={`${styles.root} ${className}`}
      data-part="stage"
      data-color-scheme={colorScheme}
      data-state={state}
      data-motion={animate ? 'on' : 'off'}
      data-reduced={reduced || undefined}
      style={{ '--led': led } as CSSProperties}
    >
      <button
        type="button"
        className={styles.pill}
        data-part="pill"
        data-holding={holding || undefined}
        aria-pressed={isRunning}
        aria-labelledby={`${id}-label ${id}-time`}
        aria-describedby={`${id}-hint`}
        onClick={onClick}
        onKeyDown={onKeyDown}
        onPointerDown={onPointerDown}
        onPointerUp={() => setHolding(false)}
        onPointerLeave={cancelHold}
        onPointerCancel={cancelHold}
        onContextMenu={(event) => event.preventDefault()}
      >
        <span id={`${id}-label`} className={styles.hidden}>
          {label}
        </span>
        <span key={cycle} className={styles.lamp} data-part="lamp" aria-hidden="true">
          {LEDS.map(({ x, y, onset }) => {
            const progress = (((hold - onset) % FRAMES) + FRAMES) % FRAMES;
            return (
              <span
                key={`${x}-${y}`}
                className={styles.led}
                data-part="led"
                data-level={level(progress)}
                style={
                  {
                    '--x': x,
                    '--y': y,
                    animationDelay: `${(-progress / FRAMES) * 1.2}s`,
                  } as CSSProperties
                }
              />
            );
          })}
        </span>
        <span className={styles.time} data-part="time" aria-hidden="true">
          {[...time].map((char, index) => (
            <span
              key={index}
              className={char === ':' ? styles.colon : styles.digit}
              data-part={char === ':' ? 'colon' : 'digit'}
            >
              {char}
            </span>
          ))}
        </span>
        <span id={`${id}-time`} className={styles.hidden}>
          {spoken(ms)}
        </span>
      </button>
      <span id={`${id}-hint`} className={styles.hidden}>
        Press to start or pause. Hold, or press R, to reset.
      </span>
      <span className={styles.hidden} role="status" aria-live="polite">
        {message}
      </span>
    </div>
  );
}
```

### dot-timer/dot-timer.module.css

```css
/* Dot timer (dot-timer.tsx): a dark pill on a soft lilac stage, a round lamp of LEDs on its left
   and the time on its right.

   Every size is in --u, a px of the pill at its own size (63 px tall). The stage sets --u from its
   own width (a container query), so the pill keeps about 40% of the stage's width, and never
   draws smaller than its own size.

   Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes
   the color-scheme it inherits from the page (light unless the page says otherwise), or the one
   its colorScheme prop sets (data-color-scheme). The pill is dark on both; the LEDs take --led,
   set by the color prop.

   Memory. One animation on 21 small boxes' opacity, in steps; paused while stopped, off screen,
   in a hidden tab or under reduced motion. */

@property --u {
  syntax: '<length>';
  inherits: true;
  initial-value: 1px;
}

.root {
  /* The stage: lilac to pink by day, deep violet at night. */
  --stage: light-dark(#d8d0fd, #16122a);
  --stage-1: light-dark(#fde0fe, #3a2050);
  --stage-2: light-dark(#bdb1fd, #2a2160);
  --stage-3: light-dark(#bcc3fd, #1b2352);
  --stage-4: light-dark(#d2d8fc, #221a40);
  /* The pill, its edge, and its edge under the pointer. */
  --pill: #18171e;
  --pill-edge: light-dark(#504f56, #45444c);
  --pill-edge-hover: light-dark(#66656d, #5c5b64);
  --pill-lift: light-dark(rgb(40 20 90 / 0), rgb(0 0 0 / 0.35));
  --ink: #fff;
  /* The LEDs at rest, and warm just before and after they light: shares of --led on the pill. */
  --dim: 0.275;
  --warm: 0.395;
  --focus: light-dark(#5b3df5, #b9a8ff);
  --led: #ff0418;
  color-scheme: inherit;
  container-type: inline-size;
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 3 / 2;
  min-height: 300px;
  max-height: 600px;
  overflow: hidden;
  overflow: clip;
  border-radius: 18px;
  background:
    radial-gradient(60% 55% at 100% 0%, var(--stage-1), transparent),
    radial-gradient(55% 60% at 8% 42%, var(--stage-2), transparent),
    radial-gradient(50% 50% at 100% 100%, var(--stage-3), transparent),
    radial-gradient(55% 45% at 45% 100%, var(--stage-4), transparent), var(--stage);
  -webkit-tap-highlight-color: transparent;
}

.root[data-color-scheme='light'] {
  color-scheme: light;
}

.root[data-color-scheme='dark'] {
  color-scheme: dark;
}

.root[data-color-scheme='system'] {
  color-scheme: light dark;
}

.pill {
  --u: clamp(1px, 100cqi * 0.4 / 143, 2.4px);
  display: flex;
  align-items: center;
  gap: calc(16.7 * var(--u));
  box-sizing: border-box;
  height: calc(63 * var(--u));
  margin: 0;
  padding: 0 calc(17.8 * var(--u)) 0 calc(14 * var(--u));
  border: calc(1 * var(--u)) solid var(--pill-edge);
  border-radius: 999px;
  background: var(--pill);
  box-shadow: 0 calc(2 * var(--u)) calc(10 * var(--u)) var(--pill-lift);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  transition:
    border-color 160ms ease,
    transform 160ms ease;
}

.pill:hover {
  border-color: var(--pill-edge-hover);
}

.pill:active {
  transform: scale(0.985);
}

/* Held towards a reset: the pill presses in over the hold. */
.pill[data-holding] {
  transform: scale(0.96);
  transition:
    border-color 160ms ease,
    transform 600ms cubic-bezier(0.3, 0, 0.2, 1);
}

.pill:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

/* The lamp: 5 by 5 places, 7.82 apart, for 5.4 px LEDs. */
.lamp {
  position: relative;
  flex: none;
  width: calc((4 * 7.82 + 5.4) * var(--u));
  height: calc((4 * 7.82 + 5.4) * var(--u));
  transition: opacity 220ms ease;
}

.root[data-state='paused'] .lamp,
.root[data-state='idle'] .lamp {
  opacity: 0.62;
}

.led {
  position: absolute;
  left: calc(var(--x) * 7.82 * var(--u));
  top: calc(var(--y) * 7.82 * var(--u));
  width: calc(5.4 * var(--u));
  height: calc(5.4 * var(--u));
  border-radius: 50%;
  background: var(--led);
  opacity: var(--dim);
  animation: sweep 1.2s infinite both;
  animation-play-state: paused;
}

.root[data-motion='on'] .led {
  animation-play-state: running;
}

/* Reduced motion: no animation; each LED holds the light of the still frame. */
.root[data-reduced] .led {
  animation: none;
}

.root[data-reduced] .led[data-level='lit'] {
  opacity: 1;
}

.root[data-reduced] .led[data-level='warm'] {
  opacity: var(--warm);
}

/* One LED's cycle, from the frame it lights: lit 16 of 36 frames, warm 3 frames either side. */
@keyframes sweep {
  0% {
    opacity: 1;
    animation-timing-function: step-end;
  }
  44.444% {
    opacity: var(--warm);
    animation-timing-function: step-end;
  }
  52.778% {
    opacity: var(--dim);
    animation-timing-function: step-end;
  }
  91.667% {
    opacity: var(--warm);
    animation-timing-function: step-end;
  }
  100% {
    opacity: 1;
  }
}

.time {
  display: flex;
  align-items: baseline;
  font-family:
    system-ui,
    -apple-system,
    'Segoe UI',
    Roboto,
    sans-serif;
  font-size: calc(25.5 * var(--u));
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  white-space: nowrap;
  /* The figures sit a touch above the pill's middle. */
  position: relative;
  top: calc(-0.67 * var(--u));
}

.digit {
  display: inline-block;
  width: 0.64em;
  text-align: center;
}

.colon {
  display: inline-block;
  width: 0.313em;
  text-align: center;
  transform: translateY(-0.01em);
}

.hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
```

### dot-timer/use-autoplay.ts

```tsx
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';

/*
 * When a demo may play on its own: while part of it is on screen and the tab is visible, never
 * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it
 * takes back over `idle` ms after their last input (never, for an idle of Infinity). `visible`
 * and `reduced` are returned too, for what must pause off screen whatever the demo does.
 */
export function useAutoplay<T extends HTMLElement>(idle = 6000) {
  const ref = useRef<T>(null);
  const [visible, setVisible] = useState(false);
  const [reduced, setReduced] = useState(true);
  const [taken, setTaken] = useState(false);
  const timer = useRef(0);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    let onScreen = false;
    const update = () => setVisible(onScreen && !document.hidden);
    const observer = new IntersectionObserver(
      ([entry]) => {
        onScreen = entry.isIntersecting;
        update();
      },
      { threshold: 0.3 },
    );
    observer.observe(element);
    document.addEventListener('visibilitychange', update);
    const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const readMotion = () => setReduced(motion.matches);
    readMotion();
    motion.addEventListener('change', readMotion);
    return () => {
      observer.disconnect();
      document.removeEventListener('visibilitychange', update);
      motion.removeEventListener('change', readMotion);
    };
  }, []);

  const take = useCallback(() => {
    setTaken(true);
    window.clearTimeout(timer.current);
    if (Number.isFinite(idle)) timer.current = window.setTimeout(() => setTaken(false), idle);
  }, [idle]);
  useEffect(() => () => window.clearTimeout(timer.current), []);

  return { ref, take, visible, reduced, taken, playing: visible && !reduced && !taken };
}
```