Transport Controls

transport-controlsControls

Play, stop, loop, a tempo you can drag, and a division that means something.

$ pnpm dlx shadcn@latest add @pluginui/core-transport-controls
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

musical-timeglow-buttonknob-coreuse-surface-pointeruse-wheeluse-first-interactioncontrol-theme
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-transport-controls.json

Usage

Connect the documented props to your application or host.

example.tsx
import { TransportControls } from "@/components/ui/transport-controls"

// Fully controlled. The host owns the clock and passes the position in.
<TransportControls
  playing={playing}
  onPlayingChange={setPlaying}
  onStop={() => { setPlaying(false); rewind() }}
  loop={loop}
  onLoopChange={setLoop}
  tempo={tempo}
  onTempoChange={setTempo}
  division={division}
  onDivisionChange={setDivision}
  beat={beatsElapsed}
/>

// Record only appears when it can do something.
<TransportControls recording={armed} onRecordChange={setArmed} /* ... */ />

// No callbacks: a status strip beside a rendered result.
<TransportControls playing loop tempo={174} division="1/16" />
About Transport Controls

A bar across the top of every sequencer-shaped interface, composed from existing controls rather than new button code. Its four keys use glow-button at a small size and its existing modes: toggle for play, loop and record; momentary for stop. This preserves the shipped halo, LED bezel, press-in seat and keyboard behavior. The monospace tempo readout responds to vertical drag, wheel, arrow keys or double-click reset. Shift gives a four-times-slower tenth-bpm adjustment anchored where Shift was pressed. The TAP key stores only timestamps. A pure median-averaged reducer in lib/musical-time calculates bpm and sends it immediately through onTempoChange, so a fumbled tap has no effect. The division row shows the value you need beside the selection: at the current tempo, choosing 1/8T displays 208 ms and 4.80 Hz. The beat indicator pulses only from a phase or beat prop, because a display timer quickly disagrees with an audio clock. The host owns the clock, as it does for lfo-visualizer's phase and step-grid's playhead. Each control is read-only when its callback is absent; without callbacks, the component is a status strip.

Props

PropTypeDefaultDescription
playing / onPlayingChangeboolean / (playing) => void—Whether the transport is rolling. The control is controlled: the key reports the state and never latches locally. Without the callback, it becomes a lamp showing that state.
onStop(event) => void—Momentary, and purely a report: whether stopping also clears playing, rewinds or disarms the record is the host's decision and the host's state. Absent hides the key.
loop / onLoopChangeboolean / (loop) => void—Loop state, same contract as play.
recording / onRecordChangeboolean / (recording) => void—Record arm. The key is hidden when the callback is absent, because a transport that cannot record should not show a control that suggests it can.
tempo / onTempoChangenumber / (bpm) => void—Tempo in bpm, clamped 20..300 (the band the session transport already uses). Drag, wheel, arrows, double-click-to-default and tap all report through the callback; without it the readout is a readout.
tempoStep / tempoFineStepnumber1 / 0.1What a drag, a wheel notch or an arrow key lands on, plain and with Shift held. Snapping goes through knob-core's snapToStep, so a tempo lands on its step exactly as a knob's value does.
defaultTemponumber120Where a double-click on the readout sends the tempo.
division / divisions / onDivisionChangeNoteDivisionId / NoteDivisionId[] / (id) => void"1/8" / 1/4, 1/8, 1/8T, 1/16The picker uses IDs from lib/musical-time's table: eighteen values from a whole note to a 1/32, each in plain, dotted and triplet flavours. Labels include 1/8, 1/8• and 1/8T.
showDivisionReadoutbooleantruePrint the current division's length and rate beside the row. This is what makes a division mean something: the same interval in milliseconds and in steps per second, the two units a host actually wires to.
phase / beatnumber | null—The host's clock, and the only thing that lights the beat indicator. phase is 0..1 inside the current beat; beat is an absolute fractional beat index and additionally tells the indicator which hits are downbeats. Nothing in the component animates a counter.
beatsPerBarnumber4Beats per bar for the downbeat accent. Beats are quarter notes; there is no time-signature model behind this value.
showTap / showTempo / showBeatboolean—Drop the parts a given transport does not want. The tap key defaults to visible whenever onTempoChange is present, and to hidden when it is not.
sizenumber52Key diameter in pixels. The tempo digits and the tap key scale off it.
recordColorstringvar(--uikit-record, #ef4444)The record key's colour is red, as it has been since 1955, and the theme has no token for it. This uses an opt-in variable with a red fallback, allowing a skin to claim --uikit-record without adding a token to control-theme.
onInteraction(event) => void—Fires once, synchronously inside the first gesture of any kind. Use it to resume a suspended AudioContext; this is why a transport is usually the first control on the page.
colorstring—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 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.