Arrangement
Transport and mixer
EQ and filters
Modulation
Utility
Instruments
Audio UI
An arrangement is tracks of clips along a song. The Timeline draws and moves them; the parts on this page hold the arrangement and fill in what a DAW's arrangement needs around it. Each is a file you own and installs on its own, and every part takes its state as values and reports changes through callbacks, so the arrangement can live in your own store and play through an engine of your own.
Installation
pnpm dlx shadcn@latest add @audio-ui/arrangement-editor
It installs every part below, with the timeline. Install a part alone by its name (@audio-ui/arranged-clip, @audio-ui/use-track-reorder…) to take only what you need. The preview above is @audio-ui/block-timeline, which wires them into a screen.
The state
arrangement(npx shadcn@latest add @audio-ui/arrangement) is the pure side:layputs a clip on a track so it covers what is under it,landdrops a dragged group (or its copies) on another track,shiftTrackandotherSelectedare what a group drop needs, andscaleValuerescales a clip when the tempo changes. Tracks are passed as an order of ids, so nothing assumes how many there are.use-arrangementis the state: tracks, clips, selection, each track's mix and height, and every operation on them. It keeps undo and redo over tracks and clips together.use-simulated-clockis a clock for a timeline that makes no sound, as a live source that loops between two points (to start sound at a position,use-transport-clockschedules against the audio clock), anduse-marquee-selectionis the rectangle that selects the clips it touches.
const arrangement = useArrangement({
duration,
getPosition: () => clock.getSnapshot(), // where a split cuts
initialClips,
initialTracks, // { id, name, color }[]
splitStep: bar / 4, // the shortest a split piece may be
});
arrangement.changeClip; // Timeline.Clip's onValueChange, by id
arrangement.commitClip; // its onValueCommitted: lands a drag, on another track too
arrangement.addTrack();
arrangement.removeTrack(id); // with its clips; any track can go, the last included
arrangement.undo();It also gives selectClip, selectClips, selectAll, selectTrack, splitAtCursor, duplicateSelection, removeClip, renameClip, setFadeShape (a fade's curve, as one undo step), clipAction (a clip's menu: duplicate, split, switch off, delete), mixOf and changeMix (gain, pan, mute, solo, color), heights and resizeTrack, renameTrack, duplicateTrack(id) (the track with its clips, placed after it), folded and toggleFold (a folded track is a strip of foldedHeight px that keeps its own height for when it opens), the time commands clearTimeSelection, copyOf, paste, removeTimeSpan, insertTimeSpan and consolidateTime (a time selection is { range, tracks }, and lib/arrangement holds them as pure functions: copyTime, clearTime, pasteTime, deleteTime, openTime, consolidate), fadeTimeSelection and crossfade (fades from a time selection, and a fade where two clips meet given to both), toggleSelected, and rescale(ratio) for a tempo change, which also clears the history since it is not part of it.
Clips
ArrangedClip (npx shadcn@latest add @audio-ui/arranged-clip) is a clip of the arrangement on its track's lane: drag its title bar to move it (with Alt, a copy), its edges to trim it into its source, its top corners to fade it, and pick a fade's curve from the menu of its handle. Double-click its title to rename it in place; right-click it for its menu (edit, rename, duplicate, split at the cursor, switch off, delete), which selects it first. A MIDI clip draws its notes across its body, and a looped clip has no end while a warped one lasts as long as its warp stretches its source: clipReach(clip) in lib/arrangement gives how far a clip can be stretched, undefined when it loops.
import { ArrangedClip, ArrangedClipGhost } from "@/components/audio-ui/arranged-clip";
<TimelineLane>
{clips
.filter((clip) => clip.track === track.id)
.map((clip) => (
<ArrangedClip
clip={clip}
describe={describe}
key={clip.id}
onAction={arrangement.clipAction}
onChange={arrangement.changeClip}
onCommit={arrangement.commitClip}
onFadeShape={arrangement.setFadeShape}
onRemove={arrangement.removeClip}
onRename={arrangement.renameClip}
onSelect={arrangement.selectClip}
selected={arrangement.selectedIds.includes(clip.id)}
swatch={arrangement.mixOf(track).color}
trackName={track.name}
/>
))}
</TimelineLane>;While a clip is dragged with Alt, arrangement.copying lists the originals: draw an ArrangedClipGhost for each, where it stays. lift and onLift move the rest of a selection up or down with the clip being dragged across tracks.
With snapping on, a dragged clip or loop also lands on the edges of the other clips, the locators and the loop, through snapPoints. Where two clips meet, the fade-out of one and the fade-in of the next move together, as a crossfade, and a dashed line shows how far a fade being dragged can go.
Tracks
A track's header is TimelineTrackControls, from the timeline: its name, gain, pan, mute, solo, color, fold and menu. useTrackReorder (npx shadcn@latest add @audio-ui/use-track-reorder) moves tracks: give each track a grip, and drag the grip, or press Up or Down on it.
import { useTrackReorder } from "@/hooks/use-track-reorder";
const reorder = useTrackReorder({
onMove: arrangement.moveTrack, // (id, index)
order: tracks.map((track) => track.id),
});
<button aria-label={`Move ${track.name}`} type="button" {...reorder.gripProps(track.id)}>
⋮⋮
</button>;The grip sits in its track's row, inside the Timeline. While a track is dragged, draggingId is the track and drop ({ id, edge }) is the edge of the row it would land on, to draw a line there; ghost is the box of its header, and ghostRef goes on the copy you draw under the pointer, fixed, which the drag moves without a render. The view scrolls when the pointer nears its top or bottom edge.
Locators and time signatures
TimelineLocator and TimelineSignature (npx shadcn@latest add @audio-ui/timeline-markers) go in a TimelineMark on a lane of their own. A locator is a named point: press it to put the cursor there, double-click it or press Enter to rename it, Delete to take it away. The block's M adds one at the cursor, and Alt with the arrows jumps from one to the next.
import { TimelineLocator, TimelineSignature } from "@/components/audio-ui/timeline-markers";
<TimelineLane>
{locators.map((locator) => (
<TimelineMark key={locator.at} value={locator.at}>
<TimelineLocator
locator={locator}
onJump={seek}
onRemove={removeLocator}
onRename={renameLocator}
/>
</TimelineMark>
))}
</TimelineLane>;The Signature strip holds time-signature changes: its + adds one at the cursor, in the signature already there, ready to be typed (such as 7/8). From a change on, bars are counted in its signature: the ruler's numbers, the hatched partial bar a change cuts short, and the position in the transport all follow, through barsOf and barsBeatsWith from @audio-ui/core/time. On a change, Left and Right move it by a grid step, Enter edits it and Delete takes it away. The grid's bar-long divisions and the tempo stay those of the song's start. Which change is being typed is yours to hold (editing and onEditingChange), so a change just added opens ready to type. parseSignature("7/8") reads what is typed.
Automation
Press A, or the automation button, for automation mode: each track's header gets a chooser of what its lane shows, Volume or Pan, and its lane the envelope of that parameter over its clips (automation-envelope, npx shadcn@latest add @audio-ui/automation-envelope). Until the track has one, the line stands faint at the mix's value; double-click it to add a point, drag a point to move it on the grid (its level stays free), drag the small handle on a segment to bend it, and double-click a point to take it away. Beside the line, a press reaches the lane and the clips as before. A MIDI track's channel follows its envelopes as it plays, glided ahead on the audio clock. Moving an automated gain or pan by hand overrides it: the envelope greys, the mix plays, and Re-enable automation in the toolbar (or drawing on the envelope) brings it back. In lib/arrangement, a track keeps its automation per parameter as points across the whole arrangement, x from 0 to 1, so a tempo change keeps them in place; automationAt reads one, volumeGain and panPosition turn its values into a gain and a pan, and mixAt gives what a track plays at a point of the song.
TrackAutomationChooser and TrackAutomation (npx shadcn@latest add @audio-ui/track-automation) are those two parts: the chooser goes in the track's header (TimelineTrackControls takes it as children), the envelope in its lane. AUTOMATED_PARAMETERS lists what a track automates, Volume and Pan.
MIDI clips
A track can also hold MIDI: Bass and Keys are MIDI tracks, and + MIDI under the tracks adds one. A MIDI clip draws its notes across its body and, picked, opens on a piano roll in the clip panel (midi-clip-panel, npx shadcn@latest add @audio-ui/midi-clip-panel), with the same strip and Clip and Loop boxes (Loop adds Dupl. Loop, which plays the loop's notes again after it) and a Notes box (transpose, invert, reverse, legato, ×2, ÷2, humanize and velocity ramps, on the selected notes or every note): draw notes a grid step at a time, drag the loop brace on its ruler, move, stretch and edit them as in the piano roll, and each edit is one undo step of the arrangement. Double-click a bare MIDI lane for an empty clip a bar long, which loops as it is stretched. Unlike the recordings, MIDI clips are heard: while the arrangement plays, their notes go to a synth, scheduled ahead on the audio clock, on the tracks that are not muted or outside a solo, each MIDI track through a channel of its own that its header's gain and pan set. In lib/arrangement, a track's kind is "audio" or "midi", a MIDI clip keeps its notes in its source's time, and playedNotes gives where they play through the clip's offset and loop.
useMidiPlayback (npx shadcn@latest add @audio-ui/use-midi-playback) plays them. Give it the clips, the tracks, the arrangement's clock and whether it plays; audible says whether a track is heard and levelsAt gives a track's gain and pan at a time, from its mix or its automation. It returns onNoteOn and onNoteOff for a MIDI clip's editor, which sound what it auditions. Its voices and its bus are the Audio engine's Synthesis and Output; with an engine of your own, leave the hook out and play the clips from playedNotes.
The clip panel
Under the tracks, the clip panel (clip-panel, npx shadcn@latest add @audio-ui/clip-panel) shows the clip you pick, as a detail view follows the selection; its × hides it until the next pick. Laid out as a DAW's clip view (clip-view holds the frame), a strip along its top carries the caret that rolls the panel up to that strip, the clip's switch, its name (type to rename it) and a word on it; under it, its properties sit in boxes beside the editor: Clip (where it starts and ends in its source), Loop (the switch, where the loop sits and how long it is, ÷2 and ×2) and Sample (Warp, Reverse, Crop, Gain and Transpose), all in bars.beats.16ths you can type or step with the arrow keys. Loop and Sample fold to a strip from their heading and give their width to the editor; what is rolled up or folded stays so from clip to clip, and the panel stays at the bottom of the block as the tracks scroll. Its source has a timeline of its own: an overview bar to zoom and scroll with, a ruler in bars, the stretch the clip plays to drag at both ends, or with Loop on the stretch that repeats (a looped clip can then be stretched past its source on the timeline), and the waveform under them. With Warp on, the source is stretched between markers that pin a time in it to a time on the clip: drag a marker, or press its arrows, to stretch what is on either side; double-click the waveform to add one, Delete to take one away, and ×2 or ÷2 to play the whole of it slower or faster. It plays by speed alone, so pitch follows (shown as a rate and its semitones), and a warped clip lasts as long as its warp stretches its source on the timeline. The map is warpedTime, unwarpedTime, warpRate and the marker functions in @audio-ui/core/time. The block makes no sound, so these are kept on the clip (gain, transpose, reversed, loop, warp, sourceFrom in lib/arrangement) and its waveform is a stand-in.
Keyboard
A drag across the bare lanes selects a stretch of time on the tracks it sweeps, drawn over them; the time commands below work on it. The block's own keys work once a press on a lane has put the focus in it:
Keys · 20
Home and End go to the start and to the end of the last clip.Left / Right move it by a grid step, Up / Down by its own length; with Cmd / Ctrl, Left / Right shorten or lengthen it and Up / Down double or halve it.Undo and redo
The undo-history lib (npx shadcn@latest add @audio-ui/undo-history) implements undo and redo as values: every function takes the present and returns the new present, and keeps no state of its own. Keep the history in a ref and the document in state, so two undos in one tick are fed the first one's result instead of reading the same stack twice.
let history = emptyHistory<Clip[]>();
// A change made in one go: delete, split, duplicate.
history = recordChange(history, clips);
// A drag reports every tick: open a burst on the first, close it on commit,
// and the whole drag is one entry.
history = beginChange(history, clips);
history = settleChange(history);
const step = undo(history, clips); // { history, present } or nullA new change clears redo, since the redo branch no longer follows from the present. HISTORY_LIMIT bounds the depth. The arrangement block uses it for its undo and redo buttons and for Cmd/Ctrl + Z, with Shift (or Y) to redo.
API reference
ArrangedClip
Props · 11
value), its source and its notes.TRACK_COLORS: its track's.additive with Shift, Cmd or Ctrl, or with multi."edit", "duplicate", "split", "toggle" or "remove".useTrackReorder
Props · 5
index.TimelineLocator and TimelineSignature
Props · 6
step with the arrows, typed, deleted.TrackAutomationChooser and TrackAutomation
Props · 7
automation.useMidiPlayback
Props · 7