Rhythm Wheel
rhythm-wheelControlsA bar as a circle, and the shape the pattern makes.
$ pnpm dlx shadcn@latest add @pluginui/core-rhythm-wheelInstall
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.
Pulls in
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-rhythm-wheel.jsonUsage
Connect the documented props to your application or host.
import { RhythmWheel } from "@/components/ui/rhythm-wheel"
import { euclidPattern, maskPulses, rotateRhythm } from "maruaudio/ui/shared/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),
)
}
/>About Rhythm Wheel
A sequencer row is a line, and a line has an end. A bar loops, which is why drum machines have always drawn it as a ring of lights. This ring holds N steps, lights the struck ones and joins consecutive onsets into the pattern's rhythm polygon. E(3,8) makes a lopsided triangle you learn to recognise; four on the floor makes a square. Rotating a pattern spins its shape without changing it, the same necklace property used by pitch-class-clock to give every major triad the same triangle. Both use the same ring convention (step 0 at twelve o'clock, clockwise), pinned bit-identical in the adjacent maths. The generator is Euclidean: pulses spread as evenly as whole steps allow. Toussaint showed this produces traditional bell and clave patterns, so two knobs take you through a catalogue of real rhythms. Velocity controls each node's size and brightness; at a dozen pixels, one cue reads as distance rather than dynamics. Accent adds a separate outline ring, so a quiet accented hit keeps both cues. The host supplies the playhead, which may be fractional; this component does not animate itself. Without onToggle, it draws a generated pattern. With onToggle, each node becomes a toggle button whose arrow keys wrap around the ring.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | number | — | Number of steps in the bar, or nodes around 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 toggles to make nodes keyboard-reachable buttons. Arrow keys move around the wrapping ring; Home/End jump to the downbeat and final step; Enter/Space toggles. Without the callback, the wheel is a read-only drawing with a summary label. Taps register on pointer down, as on a pad, so there is no drag gesture. |
| 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 circular inter-onset intervals in the centre, such as “3-3-2” for a tresillo, the way a drummer says it aloud. |
| 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 | — | Fires once on the first gesture of any kind. Use it 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.