Rhythm Wheel
rhythm-wheelControlsA sequencer row is a line, and a line has an end. A bar does not — it comes round again, which is why drum machines have always drawn it as a ring of lights. Here the ring carries N steps, the struck ones are lit, and consecutive onsets are joined into the rhythm polygon: the figure the pattern actually is. E(3,8) is a lopsided triangle you learn to recognise, four on the floor is a square, and rotating a pattern spins its shape without changing it — the same necklace fact pitch-class-clock uses to make every major triad the same triangle, drawn on the identical ring convention (step 0 at twelve o'clock, clockwise, pinned bit-identical in the maths beside it). The generator is Euclid's: pulses spread as evenly as whole steps allow, which Toussaint showed produces the world's traditional bell and clave patterns rather than arbitrary ones — so two knobs walk you through a catalogue of real rhythms. Velocity is a node's size and its brightness both, because at a dozen pixels a single cue reads as distance rather than dynamics; accent is a separate outline ring, so a quiet accented hit stays both. The playhead is a prop and may be fractional: the host owns the clock, nothing here animates itself. Leave onToggle off and it is a drawing of a generated pattern; add it and every node becomes a toggle button whose arrow keys wrap round the ring, because the ring wraps.
E(3,8) — gaps 3-3-2. Euclid spreads the pulses as evenly as whole steps allow, which is why the gaps only ever take two values one step apart, and why the defaults here are E(3,8): the 3-3-2 tresillo, a rhythm that already existed long before Bjorklund wrote the algorithm down. Rotate and the polygon spins without changing shape — the pattern is a necklace. The playhead is a prop: this demo owns the clock and advances a fractional step position at 2 steps per second, so the hand sweeps smoothly between nodes and the highlight snaps to the nearest one.
Install
Copies the source into your project. Anything it depends on comes with it.
$ npx shadcn add @syntho-ui/rhythm-wheelPulls in
First time? Add the registry to your components.json once:
{
"registries": {
"@syntho-ui": "https://engine.syntho.app/r/{name}.json"
}
}Usage
Controlled or uncontrolled, whichever suits the surrounding state.
import { RhythmWheel } from "@/components/ui/rhythm-wheel"
import { euclidPattern, maskPulses, rotateRhythm } from "@/lib/rhythm-math"
// The primary story: a generated pattern, read-only, with the host's playhead.
const pattern = rotateRhythm(euclidPattern(3, 8), rotation)
<RhythmWheel
steps={8}
pulses={maskPulses(pattern, { velocity: 0.9, accents: [0] })}
playhead={stepPosition}
showIntervals
/>
// Editable: the host owns the pulses, every tap reports out.
<RhythmWheel
steps={16}
pulses={pulses}
onToggle={(step, active) =>
setPulses((current) =>
active
? [...current, { step, velocity: 0.9 }]
: current.filter((pulse) => pulse.step !== step),
)
}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | number | — | Steps in the bar — the nodes round the ring. |
| pulses | RhythmPulse[] | — | The struck steps: { step, velocity?, accent? }. Anything absent is a silent step. Velocity drives node size and brightness together; accent adds an outline ring in the secondary colour. |
| playhead | number | null | — | Where the host's clock is, as an absolute step position. Fractional for a smooth sweep, and positions past the end of the bar fold into it, so a free-running counter passes straight through. null draws no playhead. |
| onToggle | (step, active) => void | — | Report a toggle. Present makes the nodes keyboard-reachable buttons (arrows walk round the ring and wrap, Home/End jump to the downbeat and the last step, Enter/Space toggle); absent makes the wheel a read-only drawing with a summary label. Taps act on pointer down, like a pad — there is no drag gesture to wait for. |
| showPolygon | boolean | true | Draw the inter-onset polygon under the nodes. Faint on purpose: it is the pattern's shape, not the pattern. |
| showIntervals | boolean | false | Print the circular inter-onset intervals in the middle — "3-3-2" for the tresillo, which is how a drummer says it out loud. |
| label | string | — | Centre text, overriding the interval readout. |
| groupSize | number | 4 | Steps per group tick. Ticks sit outside the ring so the beats stay countable without adding ink inside it; the downbeat's is longer and lit, because a circle has no left-hand end. 0 disables the grouping. |
| size / nodeRadius | number | 220 / 12 | Square size in pixels, and the radius of a full-velocity node. The ring is inset by the node radius so nodes sit fully inside the box. |
| onInteraction | (event) => void | — | Fired once, on the first gesture of any kind — the place to resume a suspended AudioContext. |
| color | string | — | Override the accent colour for this instance. |
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.