useBeatClock
beat-clockFoundationsWhere the playhead is, and whose clock said so.
$ pnpm dlx shadcn@latest add @pluginui/core-beat-clockInstall
Copies the source into your project. Anything it depends on comes with it. Run this in your React project with shadcn initialized; no registry configuration is needed.
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-beat-clock.jsonUsage
Connect the documented props to your application or host.
import { useBeatClock } from "@/lib/beat-clock"
const { phase, beats, source } = useBeatClock({
transport: host.transport, // { playing, tempo, ppqPosition } - omit to free-run
bpm: 128, // fallback tempo, and the free-run's rate
bars: 2,
running: !bypassed,
})
// The caption never claims a sync it does not have.
const label =
source === "host" ? "Host-locked"
: source === "free" ? `Free-running at ${bpm} BPM`
: "Stopped"About useBeatClock
A tempo-synced face must answer one question per frame: which beat is in the visible window? This hook keeps the two valid answers distinct. With a playing host transport, phase comes from ppqPosition. The window tiles along the project's quarter notes from zero, so the same musical position always appears at the same x and a loop or relocate does not shift the picture. Without a playing host transport, the hook free-runs from a bpm on a requestAnimationFrame loop. That gives a plausible illustration at the right tempo, but it does not show the host's position. running={false} parks phase at 0 instead of freezing its last value. The returned source is “host”, “free” or “stopped”, allowing a caption to say which; this prevents a free-running playhead from being presented as host sync. A stopped DAW free-runs so the face does not look frozen, while still borrowing the host's tempo because that value remains real. Exported pure functions cover the phase mappings, source rule and tempo fallback, with tests pinning their behavior. The hook only runs the loop. requestAnimationFrame, performance and window are accessed inside an effect, so the server's first render is phase 0 rather than a throw.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| transport | BeatClockTransport | null | — | Transport fields { playing, tempo, ppqPosition }. This matches the structural head of audio-contracts' TransportState, so a host state is assignable as-is. It is declared locally so an installed copy stands alone. Supplying it does not by itself lock the display to the host; it must also be playing. |
| bpm | number | 120 | Tempo for the free-run, and when the transport reports none. A host that IS reporting a usable tempo always wins, including while stopped. |
| bars / beatsPerBar | number | 2 / 4 | The window the phase is measured across. |
| running | boolean | true | False parks the display at phase 0, as required for a bypassed module or hidden panel. It takes precedence over a rolling transport. |
| returns | { phase, beats, source } | — | phase ranges from 0 to 1 across the window; beats uses the window's own units; source is “host”, “free” or “stopped”, for captions. |
| freeRunPhase / hostWindowPhase | pure functions | — | The two mappings, exported and tested: elapsed seconds at a tempo, and a ppqPosition tiled into the window. Both wrap into 0..1, both return 0 rather than NaN for an unresolved tempo or a non-finite position, and a negative pre-roll PPQ wraps forward instead of going negative. |
| resolveBeatClockSource / resolveBeatClockTempo | pure functions | — | The policy, alone and assertable: which clock is authoritative, and whose bpm the free-run should use. |
Interaction
- Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
- Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
- Double-click resets to
defaultValue. - Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
- Proper
sliderandswitchroles with livearia-valuetext, so a screen reader announces the formatted value rather than the raw number. - The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.