Beat Grid Scope

beat-grid-scopeViewers

Bars across, dB downward, and a slot where you draw the signal.

$ pnpm dlx shadcn@latest add @pluginui/core-beat-grid-scope
Loading interactive preview…

Install

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-beat-grid-scope.json

Usage

Connect the documented props to your application or host.

example.tsx
import { BeatGridScope } from "@/components/ui/beat-grid-scope"

// The consumer draws the signal; the scope owns the space it is drawn in.
<BeatGridScope
  bars={2}
  ceilingDb={14}
  transport={host.transport}      // omit for a free-run at bpm
  bpm={128}
  onClockChange={setClockSource}  // "host" | "free" | "stopped" -> the caption
  lanes={[
    { id: "kick", label: "KICK", events: [{ beat: 0 }, { beat: 2, strength: 0.6 }] },
    { id: "clap", label: "CLAP", events: [{ beat: 1, ghost: true }] },
  ]}
  caption="Predicted from the reference pattern - free-running at 128 BPM"
>
  {(layout) => (
    <path
      d={envelopePath(layout)}   // layout.xForBeat / layout.yForDb
      stroke="currentColor"
      fill="none"
    />
  )}
</BeatGridScope>
About Beat Grid Scope

The chrome of a dynamics scope, with the signal left to you. Bars run across with bar lines heavier than beats and an optional sixteenth grid. The dB axis reads downward from 0 at the top because gain reduction is depth, not height; that is the convention used by compressor bridges. A dashed line marks the hard budget. The dash signals a limit rather than a measurement. The gutter's numbers follow the same rule: they count in round decibels, and the bottom number shows the axis's actual depth instead of a decorative infinity symbol that would imply silence on a plot only three decibels deep. Beneath the plot, event lanes show onset ticks at their exact beat on the plot's own xForBeat. A swung hit therefore sits off its gridline instead of snapping to a step's middle, as it does in drum-lane. The playhead runs through plot and lanes together so an onset and the curve that answered it share one cursor. This component draws no signal itself. Its children render from the resolved layout, keeping prediction envelopes, measured waveforms and live overlays in the same coordinates as the grid. There is no second axis to keep in sync. For timing, pass a phase to own the clock (null hides the playhead for a stopped transport), or pass a host transport and useBeatClock to derive one. onClockChange reports whether the position is host-locked or free-running, so the caption can say which. Pure SVG over a fixed layout box; nothing is measured, and it renders on the server.

Props

PropTypeDefaultDescription
children(layout: BeatGridLayout) => ReactNode—The signal layer. A render function receiving the resolved layout, so your paths share the grid's exact coordinate space (xForBeat, xForSeconds, yForDb). Clipped to the plot, so a curve past the axis cannot spill over the gutter or the lanes.
renderBackground(layout: BeatGridLayout) => ReactNode—A second slot beneath the grid. Use it for a waveform that should appear behind the grid lines.
bars / beatsPerBar / subdivisionsnumber2 / 4 / 4The window. subdivisions is grid lines per beat; 4 draws the sixteenth grid that makes an off-grid onset read as off-grid, 1 draws none.
axisDb / minAxisDbnumber24 / 6How far down the dB axis reaches, and the floor on it. The floor is what stops a caller scaling the axis to live data from magnifying 0.2 dB of movement into a full-height dive.
ceilingDb / ceilingLabelnumber / string—The hard budget, drawn as a dashed labelled line and clamped into the plot so a budget past the axis lands on the floor rather than off the surface. Sign-agnostic: -14 and 14 are the same line.
phasenumber | null—Playhead position from 0 to 1 when you own the clock. Pass null to hide it, which is the accurate view for a stopped transport. Omit the prop to derive a position from transport/bpm.
transport / bpm / runningBeatClockTransport | null / number / boolean—The derived clock's inputs, passed straight to useBeatClock: a playing host transport locks the playhead to its ppqPosition, anything else free-runs at bpm, running={false} parks at 0.
onClockChange(source: BeatClockSource) => void—Reports "host" | "free" | "stopped" for the derived clock, so a caption can distinguish host-locked from free-running instead of implying a sync the face does not have. Silent when you pass phase yourself.
lanesBeatGridLane[]—The event strip: { id, label, color?, marker?, events } where an event is { beat, strength?, ghost?, recoveryBeat? }. Ticks are placed at their exact beat; strength sets height and opacity, ghost draws hollow, recoveryBeat draws a tail to where the recovery completes. marker is a one-glyph pointer drawn in the gutter beside the lane's name, so the strip itself says which lane a readout elsewhere is talking about.
laneHeight / plotHeight / viewWidthnumber16 / 150 / 320Layout-box dimensions. The SVG scales to its container's width; the box is the coordinate space your layer draws in.
hideDbAxis / dbTickCountboolean / numberfalse / 4Hide the dB gutter (and reclaim its width), or change how many labels sit below 0. dbTickCount is a target rather than a promise: the round-step chooser will land a label or two either side of it to keep the numbers whole.
axisFloorLabelstring | null—Text printed at the bottom of the gutter. By default it shows the axis's actual depth, such as “-3.0” on a three-decibel axis. A face that scales axisDb to live data cannot know the depth in advance, and a fixed “-infinity” would claim silence not shown on the screen. Pass “-infinity” when the floor really is silence, as with a fader's bottom detent. Pass null to keep the floor rule but omit its text.
niceDbTicksbooleantrueSnap gutter steps to round decibels: 1 / 2 / 3 / 6 / 12 and multiples of 12. A 3 dB axis then reads -1 / -2 / -3, while a 24 dB axis reads -6 / -12 / -18 / -24, rather than arbitrary even divisions. A step is used only if it lands exactly on the floor and stays near the requested count. If no round step fits, as with 15 dB in two steps, the even division remains and prints -7.5. False always forces even division.
caption / ariaLabelReactNode / string—Line below the scope for a prediction or measurement qualifier. It is also the plot's accessible name.
color / accent2string—Override the playhead/lane accent and the ceiling colour. Otherwise both read --uikit-accent / --uikit-accent-2, so a themed host repaints the scope for free.

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 slider and switch roles with live aria-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.