Scrubber positions things by time rather than by layout. With no tracks it is a seek bar. Give it tracks and it is an editor timeline: stacked layers of blocks sharing one playhead, each block selectable, movable and trimmable.
It is not Timeline, which lays out a sequence of steps or events down the page.
Times are in whatever unit you and the component agree on — milliseconds by convention, which is what the default m:ss formatter assumes. Pass formatTime for anything else, including frames or bars.
Each track is a row of blocks. movable lets the body be dragged along the track, resizable puts trim handles on the edges, and onAdd turns a click on empty track space into a new block. Blocks colour themselves from a scheme, so layers stay distinguishable.
onBlockChange hands back absolute times, already clamped to [0, duration] and measured from where the gesture started — not accumulated per pointer move, so a block cannot drift away from the pointer over a long drag. Everything past that is your rule: a minimum length, no overlap with the next block, snapping to a grid. The block renders wherever you put it, so refusing a change is simply not applying it.
onGestureStart fires once when a drag begins, which is the moment to snapshot for undo — one entry per gesture rather than one per pointer move.
The track is a role="slider": ← and → step the playhead by step (a hundredth of the duration by default), Home and End jump to either edge. Dragging uses pointer events, so blocks and playhead work with a mouse, a pen or a finger.
Block gestures stay pointer-driven. Right-clicking a block calls onBlockContextMenu with the event and the ids — build the menu yourself, since what belongs in it is entirely your application's business.