# Block timeline

> An arrangement timeline made for a whole screen: a bar ruler that seeks on the grid, a loop strip, a lane per track with clips to move between tracks, trim and fade on the grid, a cursor to put anywhere on a track, and a split at the cursor, a transport with play or pause, stop, loop, a metronome and a tempo and signature that keep the arrangement's musical positions, a playhead following its clock, and a navigator to zoom and scroll, open it at full size and the tracks take the screen

A shadcn/ui block from the [@audio-ui](https://www.audio-ui.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/audio-ui/block-timeline).

- Page, with a live preview: https://shadcn.exchange/r/audio-ui/block-timeline
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/audio-ui/block-timeline-light-6cef0b627594667a.png
- Registry item: https://www.audio-ui.dev/r/new-york-v4/block-timeline.json
- Listed: 2026-10-17, source last changed 2026-10-17

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/audio-ui/block-timeline.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@audio-ui/core`
- Registry items: `@audio-ui/timeline`, `@audio-ui/transport`, `@audio-ui/bpm-control`, `@audio-ui/meter`, `@audio-ui/use-simulated-levels`, `@audio-ui/arrangement`, `@audio-ui/use-arrangement`, `@audio-ui/use-marquee-selection`, `@audio-ui/use-simulated-clock`, `@shadcn/button`, `@shadcn/button-group`, `@shadcn/context-menu`, `@shadcn/dropdown-menu`, `@shadcn/kbd`, `@shadcn/popover`, `@shadcn/select`, `@shadcn/toggle`

## Files

### `block-timeline.tsx`

```tsx
"use client";

import {
  barLabelInterval,
  DEFAULT_TEMPO,
  describeBarsBeats,
  GRID_DIVISIONS,
  type GridDivision,
  gridInterval,
  gridLabel,
  type MetronomeRhythm,
  secondsPerBar,
  type Tempo,
} from "@audio-ui/core/time";
import {
  adjacentPoint,
  type FadeShape,
  type Timeline as TimelinePrimitive,
} from "@audio-ui/react";
import {
  type FocusEvent,
  Fragment,
  type KeyboardEvent,
  type MouseEvent,
  memo,
  type PointerEvent,
  type ReactNode,
  type SyntheticEvent,
  useCallback,
  useLayoutEffect,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import { NumberControl } from "@/components/audio-ui/bpm-control";
import { Meter } from "@/components/audio-ui/meter";
import {
  COMPACT_TRACK_HEIGHT,
  MAX_TRACK_HEIGHT,
  MIN_TRACK_HEIGHT,
  Timeline,
  TimelineClip,
  TimelineClipFade,
  TimelineClipFadeHandle,
  TimelineClipHandle,
  TimelineClipHeader,
  TimelineGridMenuItems,
  TimelineLane,
  TimelineMark,
  TimelineNavigator,
  TimelinePlayhead,
  TimelineRange,
  TimelineRangeHandle,
  TimelineRuler,
  TimelineTrack,
  TimelineTrackControls,
  TimelineTrackHeader,
  TimelineViewport,
  TRACK_COLORS,
  TRACK_HEIGHT_STEP,
} from "@/components/audio-ui/timeline";
import { Transport } from "@/components/audio-ui/transport";
import {
  type ClipAction,
  useArrangement,
} from "@/hooks/use-arrangement";
import { useMarquee } from "@/hooks/use-marquee-selection";
import { useSimulatedClock } from "@/hooks/use-simulated-clock";
import { useSimulatedLevels } from "@/hooks/use-simulated-levels";
import type {
  Arranged,
  ArrangementTrack,
} from "@/lib/arrangement";

const BARS = 32;
/** The arrangement is laid out at the first tempo; a tempo change keeps its musical positions. */
const BAR = secondsPerBar(DEFAULT_TEMPO);
/** The width of a track header, which the lanes start after. */
const LEAD_PX = 160;
const MAX_ZOOM = 240;
/** The zoom presets, as shares of the fitted view: Fit shows everything. */
const ZOOM_PRESETS = [1, 2, 4, 8];
/** The zoom's steps are loupes, not a minus and a plus. */
const ZOOM_STEPPERS = {
  decrement: "Zoom out",
  icons: {
    decrement: (
      <IconPlaceholder
        hugeicons="ZoomOutAreaIcon"
        lucide="ZoomOutIcon"
        phosphor="MagnifyingGlassMinusIcon"
        remixicon="RiZoomOutLine"
        tabler="IconZoomOut"
      />
    ),
    increment: (
      <IconPlaceholder
        hugeicons="ZoomInAreaIcon"
        lucide="ZoomInIcon"
        phosphor="MagnifyingGlassPlusIcon"
        remixicon="RiZoomInLine"
        tabler="IconZoomIn"
      />
    ),
  },
  increment: "Zoom in",
};

/** How the zoom is shown: as a share of the fitted view. */
const PERCENT: Intl.NumberFormatOptions = {
  maximumFractionDigits: 0,
  style: "percent",
};

const bars = (count: number) => count * BAR;

const TRACKS: ArrangementTrack[] = [
  { color: 0, id: "drums", name: "Drums" },
  { color: 1, id: "bass", name: "Bass" },
  { color: 2, id: "keys", name: "Keys" },
  { color: 3, id: "vocals", name: "Vocals" },
] as const;

/** A clip's menu: what is done to it, then what is done away with. */
function clipMenuEntries(disabled: boolean): {
  action: ClipAction;
  destructive?: boolean;
  label: string;
  separatorBefore?: boolean;
}[] {
  return [
    { action: "rename", label: "Rename" },
    { action: "duplicate", label: "Duplicate" },
    { action: "split", label: "Split at the cursor", separatorBefore: true },
    {
      action: "toggle",
      label: disabled ? "Switch clip on" : "Switch clip off",
    },
    {
      action: "remove",
      destructive: true,
      label: "Delete",
      separatorBefore: true,
    },
  ];
}

const selectText = (element: HTMLInputElement | null) => element?.select();

/** A clip's name edited where it is written: Enter keeps it, Escape gives it back. */
function ClipNameField({
  name,
  onCommit,
  onDone,
}: {
  name: string;
  onCommit: (name: string) => void;
  onDone: () => void;
}) {
  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLInputElement>) => {
      // The clip drags, deletes and splits on its own keys, and so does the panel: a name must not.
      event.stopPropagation();
      if (event.key === "Enter") {
        onCommit(event.currentTarget.value);
        onDone();
      } else if (event.key === "Escape") {
        onDone();
      }
    },
    [onCommit, onDone]
  );
  const handleBlur = useCallback(
    (event: FocusEvent<HTMLInputElement>) => {
      onCommit(event.currentTarget.value);
      onDone();
    },
    [onCommit, onDone]
  );
  const stop = useCallback(
    (event: SyntheticEvent) => event.stopPropagation(),
    []
  );
  return (
    <input
      aria-label="Clip name"
      className="min-w-0 flex-1 bg-transparent outline-none"
      defaultValue={name}
      onBlur={handleBlur}
      onClick={stop}
      onDoubleClick={stop}
      onKeyDown={handleKeyDown}
      onPointerDown={stop}
      ref={selectText}
    />
  );
}

/** What a track meter shows while nothing plays: one channel. */
const SILENCE_MONO = {
  getSnapshot: () => ({ levels: [-60], peaks: [-60] }),
  subscribe: () => () => undefined,
};

/** What the master meter shows while nothing plays. */
const SILENCE = {
  getSnapshot: () => ({ levels: [-60, -60], peaks: [-60, -60] }),
  subscribe: () => () => undefined,
};

type Span = readonly [number, number];

const arranged = (
  id: string,
  name: string,
  track: string,
  from: number,
  to: number
): Arranged => ({
  id,
  name,
  sourceLength: bars(to - from + 2),
  track,
  value: {
    fadeIn: 0,
    fadeOut: 0,
    length: bars(to - from),
    offset: 0,
    start: bars(from),
  },
});

const CLIPS: Arranged[] = [
  arranged("d1", "Intro", "drums", 0, 4),
  arranged("d2", "Groove", "drums", 4, 16),
  arranged("d3", "Groove", "drums", 16, 28),
  arranged("d4", "Fill", "drums", 28, 32),
  arranged("b1", "Sub", "bass", 4, 12),
  arranged("b2", "Sub", "bass", 16, 28),
  arranged("k1", "Pad", "keys", 0, 8),
  arranged("k2", "Chords", "keys", 8, 16),
  arranged("k3", "Pad", "keys", 20, 32),
  arranged("v1", "Verse", "vocals", 8, 16),
  arranged("v2", "Hook", "vocals", 16, 24),
];

/** A row of the arrangement: the track's header stuck to the left edge as it scrolls, its lane beside it. */
/** The nearest ancestor that scrolls vertically, or null for the page. */
function verticalScroller(node: HTMLElement): HTMLElement | null {
  for (let parent = node.parentElement; parent; parent = parent.parentElement) {
    const { overflowY } = getComputedStyle(parent);
    if (
      (overflowY === "auto" || overflowY === "scroll") &&
      parent.scrollHeight > parent.clientHeight
    ) {
      return parent;
    }
  }
  return null;
}

/** How far to scroll so a pointer held near the top or bottom edge of a view keeps it going; 0 away from the edges. */
function edgeScrollStep(y: number, top: number, bottom: number): number {
  if (y < top + 48) {
    return -Math.min(24, Math.ceil((top + 48 - y) / 3));
  }
  if (y > bottom - 48) {
    return Math.min(24, Math.ceil((y - bottom + 48) / 3));
  }
  return 0;
}

/** The line a track would land on: none when it stays where it is or one place below. */
function dropLine(
  gap: number,
  from: number,
  rows: { id: string }[]
): { id: string; edge: "bottom" | "top" } | null {
  if (gap === from || gap === from + 1) {
    return null;
  }
  if (gap < rows.length) {
    return { id: rows[gap].id, edge: "top" };
  }
  return { id: rows.at(-1)?.id ?? "", edge: "bottom" };
}

function Row({
  children,
  className,
  height,
  lead,
  onResize,
  selected,
  trackId,
}: {
  children: ReactNode;
  className?: string;
  /** A height set by hand: the row keeps it instead of sharing what is left. */
  height?: number;
  lead?: ReactNode;
  onResize?: (trackId: string, height: number | undefined) => void;
  selected?: boolean;
  trackId?: string;
}) {
  const handleHeight = useCallback(
    (next: number | undefined) => {
      if (trackId !== undefined) {
        onResize?.(trackId, next);
      }
    },
    [onResize, trackId]
  );
  return (
    <TimelineTrack
      className={className}
      height={height}
      maxHeight={MAX_TRACK_HEIGHT}
      minHeight={MIN_TRACK_HEIGHT}
      onHeightChange={handleHeight}
      selected={selected}
      step={TRACK_HEIGHT_STEP}
    >
      <TimelineTrackHeader className="relative flex items-stretch">
        {lead}
      </TimelineTrackHeader>
      <div className="flex flex-1 flex-col">{children}</div>
    </TimelineTrack>
  );
}

/** Adaptive follows the zoom; the others stay put however far you zoom. */
const DIVISIONS: { label: string; value: GridDivision }[] = [
  { label: "Grid: Adaptive", value: "adaptive" },
  ...GRID_DIVISIONS.map((entry) => ({
    label:
      "bars" in entry
        ? `Grid: ${entry.bars} ${entry.bars === 1 ? "bar" : "bars"}`
        : `Grid: ${entry.id}`,
    value: entry.id as GridDivision,
  })),
];

/** What the keyboard does, for the shortcuts popover: the keys, then what they do. */
const SHORTCUTS: { action: string; keys: string[] }[] = [
  { action: "Split at the cursor", keys: ["S"] },
  { action: "Split at the cursor", keys: ["Cmd/Ctrl", "E"] },
  { action: "Duplicate the clip", keys: ["Cmd/Ctrl", "D"] },
  { action: "Remove the clip, or the tracks picked", keys: ["Delete"] },
  { action: "Add a track to the tracks picked", keys: ["Shift", "Click"] },
  { action: "Duplicate the tracks picked", keys: ["Cmd/Ctrl", "D"] },
  { action: "Add a locator at the cursor", keys: ["M"] },
  { action: "Loop the selection", keys: ["Cmd/Ctrl", "L"] },
  { action: "Rename a locator", keys: ["Enter"] },
  {
    action: "Jump to the previous or next locator",
    keys: ["Alt", "Left", "Right"],
  },
  { action: "Nudge a clip or an edge", keys: ["Left", "Right"] },
  { action: "Nudge further", keys: ["Shift", "Left", "Right"] },
  { action: "Copy a clip while dragging", keys: ["Alt", "Drag"] },
  { action: "Move or trim off the grid", keys: ["Cmd/Ctrl", "Drag"] },
  { action: "Zoom around the pointer", keys: ["Cmd/Ctrl", "Scroll"] },
  { action: "Resize a track", keys: ["Up", "Down"] },
  { action: "Add a clip to the selection", keys: ["Shift", "Click"] },
  { action: "Undo", keys: ["Cmd/Ctrl", "Z"] },
  { action: "Redo", keys: ["Cmd/Ctrl", "Shift", "Z"] },
  { action: "Select every clip", keys: ["Cmd/Ctrl", "A"] },
  { action: "Select by dragging a rectangle", keys: ["Shift", "Drag"] },
  { action: "Clear the selection", keys: ["Esc"] },
];

interface Locator {
  at: number;
  name: string;
}

/**
 * A locator: press it to put the cursor there, double-click it or press
 * Enter to rename it (Enter keeps the name, Escape drops the change), Delete
 * takes it away.
 */
function LocatorButton({
  locator,
  onJump,
  onRemove,
  onRename,
}: {
  locator: Locator;
  onJump: (at: number) => void;
  onRemove: (at: number) => void;
  onRename: (at: number, name: string) => void;
}) {
  const { at, name } = locator;
  const [editing, setEditing] = useState(false);
  const handleClick = useCallback(() => onJump(at), [at, onJump]);
  const startEditing = useCallback(() => setEditing(true), []);
  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>) => {
      if (event.key === "Delete" || event.key === "Backspace") {
        event.preventDefault();
        event.stopPropagation();
        onRemove(at);
      } else if (event.key === "Enter") {
        event.preventDefault();
        event.stopPropagation();
        setEditing(true);
      }
    },
    [at, onRemove]
  );
  const handleEditKeyDown = useCallback(
    (event: KeyboardEvent<HTMLInputElement>) => {
      event.stopPropagation();
      if (event.key === "Enter") {
        const next = event.currentTarget.value.trim();
        if (next) {
          onRename(at, next);
        }
        setEditing(false);
      } else if (event.key === "Escape") {
        setEditing(false);
      }
    },
    [at, onRename]
  );
  const handleBlur = useCallback(() => setEditing(false), []);
  const handleRemove = useCallback(() => onRemove(at), [at, onRemove]);
  // The menu hands focus back as it closes, which would end the edit: open the field after it.
  const renameLater = useRef(false);
  const renameWhenClosed = useCallback(() => {
    renameLater.current = true;
  }, []);
  const handleMenuClosed = useCallback((open: boolean) => {
    if (!open && renameLater.current) {
      renameLater.current = false;
      setEditing(true);
    }
  }, []);

  if (editing) {
    return (
      <input
        aria-label="Locator name"
        autoFocus
        className="h-full w-24 border-primary border-s-2 bg-background px-1 text-primary text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
        defaultValue={name}
        onBlur={handleBlur}
        onKeyDown={handleEditKeyDown}
        onPointerDown={stopPress}
      />
    );
  }
  return (
    <ContextMenu onOpenChangeComplete={handleMenuClosed}>
      <ContextMenuTrigger
        render={
          <button
            aria-label={`Locator ${name}`}
            className="pointer-events-auto flex h-full items-center whitespace-nowrap border-primary border-s-2 ps-1 pe-2 text-primary text-xs outline-none hover:bg-primary/10 focus-visible:ring-2 focus-visible:ring-ring"
            onClick={handleClick}
            onDoubleClick={startEditing}
            onKeyDown={handleKeyDown}
            type="button"
          />
        }
      >
        {name}
      </ContextMenuTrigger>
      <ContextMenuContent className="w-auto min-w-32">
        <ContextMenuItem onClick={renameWhenClosed}>Rename</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem onClick={handleRemove} variant="destructive">
          Delete
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  );
}

/** Keeps a press inside a field from reaching the lane under it. */
function stopPress(event: PointerEvent<HTMLElement>) {
  event.stopPropagation();
}

/** The original of a clip being copied, left in its place while the copy is dragged away. */
function CopyGhost({ clip }: { clip: Arranged }) {
  const { length, start } = clip.value;
  return (
    <div
      aria-hidden="true"
      className="pointer-events-none absolute inset-y-1.5 overflow-hidden rounded-[inherit] border border-primary border-dashed bg-primary/10 px-2 text-primary text-xs"
      style={{
        left: `calc(var(--timeline-unit) * ${start})`,
        width: `calc(var(--timeline-unit) * ${length})`,
      }}
    >
      {clip.name}
    </div>
  );
}

/** A track's level, from a stand-in source: the block plays nothing, so it moves while the track would be heard. */
function TrackLevel({ active, name }: { active: boolean; name: string }) {
  const simulated = useSimulatedLevels(1);
  return (
    <Meter
      aria-label={`${name} level`}
      className="data-[orientation=vertical]:h-full [--meter-thickness:--spacing(2)]"
      orientation="vertical"
      peakMarks="hidden"
      size="sm"
      source={active ? simulated : SILENCE_MONO}
      warnThreshold={-12}
    />
  );
}

/** One clip: move it, trim either edge into its recording, fade it in and out. */
const ClipView = memo(function ArrangedClip({
  clip,
  onChange,
  describe,
  onCommit,
  onRemove,
  onSelect,
  selected,
  copying,
  lift,
  onLift,
  multi,
  onAction,
  onFadeShape,
  onRename,
  swatch,
  trackName,
}: {
  clip: Arranged;
  /** Whether a press adds the clip to the selection, as Shift does: a touch screen has no Shift. */
  multi: boolean;
  /** A fade's curve was picked from the menu of its handle. */
  onFadeShape: (id: string, edge: "in" | "out", shape: FadeShape) => void;
  /** The clip's color, an index into `TRACK_COLORS`: the track's. */
  swatch: number;
  /** Something chosen from the clip's menu (renaming is the clip's own). */
  onAction: (action: ClipAction, id: string) => void;
  onRename: (id: string, name: string) => void;
  /** How far, in px, the clip is held up or down by a group drag that is not its own. */
  lift: number;
  /** Tells the arrangement how far this clip is dragged up or down, for the rest of its group to follow. */
  onLift: (id: string, offset: number) => void;
  /** Whether this clip is being dragged with Alt held: it is the copy, and the original stays behind. */
  copying: boolean;
  describe: (seconds: number) => string;
  onChange: (
    id: string,
    value: TimelinePrimitive.Clip.Value,
    details: TimelinePrimitive.Clip.ChangeEventDetails
  ) => void;
  onCommit: (
    id: string,
    details: TimelinePrimitive.Clip.CommitEventDetails
  ) => void;
  onRemove: (id: string) => void;
  onSelect: (id: string, additive: boolean) => void;
  selected: boolean;
  trackName: string;
}) {
  const handleRemove = useCallback(
    () => onRemove(clip.id),
    [clip.id, onRemove]
  );
  const handleFadeInShape = useCallback(
    (shape: FadeShape) => onFadeShape(clip.id, "in", shape),
    [clip.id, onFadeShape]
  );
  const handleFadeOutShape = useCallback(
    (shape: FadeShape) => onFadeShape(clip.id, "out", shape),
    [clip.id, onFadeShape]
  );
  const [renaming, setRenaming] = useState(false);
  const stopRenaming = useCallback(() => setRenaming(false), []);
  const startRenaming = useCallback(() => setRenaming(true), []);
  // The name field takes focus once the menu has closed: closing hands focus back to the clip, which would end the edit.
  const renameWhenClosed = useRef(false);
  const handleMenuClosed = useCallback(
    (open: boolean) => {
      if (!open && renameWhenClosed.current) {
        renameWhenClosed.current = false;
        startRenaming();
      }
    },
    [startRenaming]
  );
  const handleMenuItem = useCallback(
    (event: MouseEvent<HTMLElement>) => {
      const action = event.currentTarget.dataset.action as ClipAction;
      if (action === "rename") {
        renameWhenClosed.current = true;
      } else {
        onAction(action, clip.id);
      }
    },
    [clip.id, onAction]
  );
  const handleRename = useCallback(
    (name: string) => onRename(clip.id, name),
    [clip.id, onRename]
  );
  // Right-clicking a clip selects it first, so the menu acts on what was pressed.
  const handleMenuOpen = useCallback(
    (open: boolean) => {
      if (open) {
        onSelect(clip.id, false);
      }
    },
    [clip.id, onSelect]
  );
  const handleSelected = useCallback(
    (
      _selected: boolean,
      details: TimelinePrimitive.Clip.SelectEventDetails
    ) => {
      const { event } = details;
      onSelect(
        clip.id,
        multi || event.shiftKey || event.metaKey || event.ctrlKey
      );
    },
    [clip.id, multi, onSelect]
  );
  const handleChange = useCallback(
    (
      value: TimelinePrimitive.Clip.Value,
      details: TimelinePrimitive.Clip.ChangeEventDetails
    ) => onChange(clip.id, value, details),
    [clip.id, onChange]
  );
  const handleDragOffset = useCallback(
    (offset: number) => onLift(clip.id, offset),
    [clip.id, onLift]
  );
  const handleCommitted = useCallback(
    (
      _value: TimelinePrimitive.Clip.Value,
      details: TimelinePrimitive.Clip.CommitEventDetails
    ) => onCommit(clip.id, details),
    [clip.id, onCommit]
  );
  return (
    <ContextMenu
      onOpenChange={handleMenuOpen}
      onOpenChangeComplete={handleMenuClosed}
    >
      <ContextMenuTrigger render={<div className="contents" />}>
        <TimelineClip
          additive={multi}
          aria-label={`${trackName}, ${clip.name}`}
          className={cn(
            "inset-y-1.5",
            TRACK_COLORS[swatch]?.clip,
            copying && "opacity-90 shadow-lg ring-2 ring-primary",
            clip.disabled && "opacity-40"
          )}
          data-clip-id={clip.id}
          getAriaValueText={describe}
          onDragOffsetChange={handleDragOffset}
          onRemove={handleRemove}
          onSelectedChange={handleSelected}
          onValueChange={handleChange}
          onValueCommitted={handleCommitted}
          selected={selected}
          sourceLength={clip.sourceLength}
          style={
            lift
              ? { transform: `translateY(${lift}px)`, zIndex: 10 }
              : undefined
          }
          value={clip.value}
        >
          <TimelineClipHeader
            className={copying ? "cursor-copy" : undefined}
            onDoubleClick={startRenaming}
          >
            {renaming ? (
              <ClipNameField
                name={clip.name}
                onCommit={handleRename}
                onDone={stopRenaming}
              />
            ) : (
              <span>{copying ? `+ ${clip.name}` : clip.name}</span>
            )}
          </TimelineClipHeader>
          <TimelineClipFade edge="in" />
          <TimelineClipFade edge="out" />
          <TimelineClipHandle aria-label={`${clip.name} start`} edge="start" />
          <TimelineClipHandle aria-label={`${clip.name} end`} edge="end" />
          <TimelineClipFadeHandle
            aria-label={`${clip.name} fade in`}
            edge="in"
            onShapeChange={handleFadeInShape}
            shape={clip.value.fadeInShape}
          />
          <TimelineClipFadeHandle
            aria-label={`${clip.name} fade out`}
            edge="out"
            onShapeChange={handleFadeOutShape}
            shape={clip.value.fadeOutShape}
          />
        </TimelineClip>
      </ContextMenuTrigger>
      <ContextMenuContent className="w-auto min-w-44">
        {clipMenuEntries(Boolean(clip.disabled)).map((entry) => (
          <Fragment key={entry.action}>
            {entry.separatorBefore ? <ContextMenuSeparator /> : null}
            <ContextMenuItem
              data-action={entry.action}
              onClick={handleMenuItem}
              variant={entry.destructive ? "destructive" : "default"}
            >
              {entry.label}
            </ContextMenuItem>
          </Fragment>
        ))}
      </ContextMenuContent>
    </ContextMenu>
  );
});

/**
 * An arrangement: a bar ruler that seeks on the grid, a loop strip, a lane per
 * track with clips to move between tracks, trim and fade, a cursor to put
 * anywhere on a track and split at, and a playhead following a transport
 * clock. Cmd/Ctrl + scroll zooms around the pointer, and the bar below scrolls
 * and zooms. Made to be opened at full size, where the tracks take the screen.
 */
export function BlockTimeline() {
  const [tempo, setTempo] = useState<Tempo>(DEFAULT_TEMPO);
  const bar = secondsPerBar(tempo);
  const duration = BARS * bar;
  const describe = useCallback(
    (seconds: number) => describeBarsBeats(seconds, tempo),
    [tempo]
  );
  const [loop, setLoop] = useState<Span>([bars(8), bars(16)]);
  const [looping, setLooping] = useState(true);
  // Locators: named points to jump to, kept in seconds at the tempo they were set.
  const [locators, setLocators] = useState<Locator[]>([
    { at: bars(4), name: "Verse" },
    { at: bars(20), name: "Chorus" },
  ]);
  const [metronome, setMetronome] = useState(false);
  const [rhythm, setRhythm] = useState<MetronomeRhythm>("auto");
  const { clock, playing, setPlaying } = useSimulatedClock({
    duration,
    loop,
    looping,
  });
  // The timeline fits the width it is given until it is zoomed.
  const viewportRef = useRef<HTMLDivElement>(null);
  const [fitPx, setFitPx] = useState(0);
  // Whether the bar has room for the zoom control; the menu lists the zoom steps only when it does not.
  const zoomSlotRef = useRef<HTMLDivElement>(null);
  const [zoomInBar, setZoomInBar] = useState(true);
  // Below the bar's room, the tempo and the undo and redo move into the more-controls menu.
  const narrowProbeRef = useRef<HTMLDivElement>(null);
  const [narrow, setNarrow] = useState(false);
  useLayoutEffect(() => {
    const element = viewportRef.current;
    if (!element) {
      return;
    }
    // The header column is narrower in a narrow panel: fit what is left beside it.
    const measure = () => {
      const lead =
        element.querySelector<HTMLElement>(
          '[data-slot="timeline-track-header"]'
        )?.offsetWidth ?? LEAD_PX;
      setFitPx(Math.max(0, (element.clientWidth - lead) / duration));
      if (zoomSlotRef.current) {
        setZoomInBar(getComputedStyle(zoomSlotRef.current).display !== "none");
      }
      if (narrowProbeRef.current) {
        setNarrow(getComputedStyle(narrowProbeRef.current).display !== "none");
      }
    };
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(element);
    return () => observer.disconnect();
  }, [duration]);
  const [zoomed, setZoom] = useState<number | null>(null);
  const zoom = Math.max(zoomed ?? fitPx, fitPx) || 8;
  const getPosition = useCallback(() => clock.getSnapshot(), [clock]);
  const {
    addTrack,
    canRedo,
    canUndo,
    changeClip,
    changeMix,
    clearSelection,
    clipAction,
    clips,
    commitClip,
    copying,
    duplicateSelection,
    duplicateTrack,
    folded,
    heights,
    toggleFold,
    mixOf,
    redo,
    moveTrack,
    removeClip,
    removeTrack,
    renameClip,
    renameTrack,
    setFadeShape,
    rescale,
    resizeTrack,
    selectAll,
    selectClip,
    selectClips,
    selectedIds,
    selectedTracks,
    selection,
    selectTrack,
    splitAtCursor,
    tracks,
    undo,
  } = useArrangement({
    colors: TRACK_COLORS.length,
    duration,
    getPosition,
    initialClips: CLIPS,
    initialTracks: TRACKS,
    splitStep: bar / 4,
  });
  const handleAddTrack = useCallback(() => addTrack(), [addTrack]);
  // Multi-select: a press on a clip adds it to the selection, and a drag on bare lane draws a rectangle. For touch, which has no Shift.
  const [multi, setMulti] = useState(false);
  // A touch screen has no Shift: in multi-select mode every press on a header adds to the tracks picked.
  const pickTrack = useCallback(
    (id: string, additive: boolean) => selectTrack(id, additive || multi),
    [multi, selectTrack]
  );
  const marquee = useMarquee(selectedIds, selectClips, multi);
  // A clip of a selection being dragged up or down, and how far: the rest of the selection follows it.
  const [lifting, setLifting] = useState<{ id: string; offset: number } | null>(
    null
  );
  const lift = useCallback(
    (id: string, offset: number) =>
      setLifting(offset === 0 ? null : { id, offset }),
    []
  );
  const [division, setDivision] = useState<GridDivision>("adaptive");
  const [triplet, setTriplet] = useState(false);
  const [snap, setSnap] = useState(true);
  // A stand-in for the mix's levels: the block plays nothing, so they move while it runs.
  const simulated = useSimulatedLevels(2);
  // What is heard: a muted track is silent, and while any track is soloed so is every other.
  const anySolo = tracks.some((track) => mixOf(track).soloed);

  const playPause = useCallback(() => setPlaying((on) => !on), [setPlaying]);
  // Stop goes back to the top, as a DAW's does.
  const stop = useCallback(() => {
    setPlaying(false);
    clock.set(0);
  }, [clock, setPlaying]);
  // A tempo change keeps the musical positions: everything is rescaled to the new bar.
  const changeTempo = useCallback(
    (next: Tempo) => {
      // New bar over old: a clip three bars long stays three bars long.
      const ratio = secondsPerBar(next) / secondsPerBar(tempo);
      rescale(ratio);
      setLoop(([start, end]) => [start * ratio, end * ratio]);
      setZoom((current) => (current === null ? null : current / ratio));
      clock.set(clock.getSnapshot() * ratio);
      setTempo(next);
    },
    [clock, rescale, tempo]
  );
  // The zoom as a share of the fitted view: 100% shows everything.
  const zoomRatio = fitPx > 0 ? zoom / fitPx : 1;
  const maxRatio = fitPx > 0 ? MAX_ZOOM / fitPx : 1;
  const zoomPreset = ZOOM_PRESETS.find(
    (ratio) => Math.abs(ratio - zoomRatio) < 0.01
  );
  const changeZoom = useCallback(
    (ratio: number) => setZoom(ratio <= 1 ? null : ratio * fitPx),
    [fitPx]
  );
  // The track being dragged by its grip, and the gap it would land in: above or below the track under the pointer.
  // The drag runs on pointer events, not the browser's drag and drop. The copy follows the pointer through a transform
  // written straight onto its element, and the rows are measured once, so a move costs no render: React only hears
  // about the drop gap when it changes.
  const [dragId, setDragId] = useState<string | null>(null);
  const [drop, setDrop] = useState<{
    id: string;
    edge: "bottom" | "top";
  } | null>(null);
  const [copy, setCopy] = useState<{
    edge: string;
    name: string;
    top: number;
    left: number;
    width: number;
    height: number;
  } | null>(null);
  const copyRef = useRef<HTMLDivElement>(null);
  const startTrackDrag = useCallback(
    (event: React.PointerEvent<HTMLButtonElement>) => {
      if (event.button !== 0) {
        return;
      }
      event.preventDefault();
      const id = event.currentTarget.dataset.trackId ?? "";
      const row = event.currentTarget.closest<HTMLElement>(
        '[data-slot="timeline-track"]'
      );
      const panel = event.currentTarget.closest<HTMLElement>(".rounded-xl");
      if (!(row && panel)) {
        return;
      }
      // Each grip spans its row's height, so its box gives the row's top and bottom.
      const rows = Array.from(
        panel.querySelectorAll<HTMLElement>("button[data-track-id]")
      ).map((grip) => {
        const box = grip.getBoundingClientRect();
        return {
          id: grip.dataset.trackId ?? "",
          top: box.top,
          bottom: box.bottom,
        };
      });
      const from = rows.findIndex((item) => item.id === id);
      const box = row.getBoundingClientRect();
      // The copy is the track's header, the column the name and the controls sit in.
      const header = (
        row.querySelector<HTMLElement>('[data-slot="timeline-track-header"]') ??
        row
      ).getBoundingClientRect();
      const scroller = verticalScroller(row);
      const scrollTop = () => (scroller ? scroller.scrollTop : window.scrollY);
      const startScroll = scrollTop();
      const startY = event.clientY;
      let gap = from;
      let lastY = startY;
      let frame = 0;

      // Once a frame while the track is dragged, whether or not the pointer moved: near an edge of the scroller, it
      // scrolls; the rows are where they were at the press, moved by how far the scroller has gone since.
      const tick = () => {
        const view = scroller
          ? scroller.getBoundingClientRect()
          : { top: 0, bottom: window.innerHeight };
        const step = edgeScrollStep(lastY, view.top, view.bottom);
        if (scroller) {
          scroller.scrollTop += step;
        } else {
          window.scrollBy(0, step);
        }
        if (copyRef.current) {
          copyRef.current.style.transform = `translateY(${lastY - startY}px)`;
        }
        const shift = scrollTop() - startScroll;
        const index = rows.findIndex(
          (item) => lastY < (item.top + item.bottom) / 2 - shift
        );
        const next = index === -1 ? rows.length : index;
        if (next !== gap) {
          gap = next;
          setDrop(dropLine(gap, from, rows));
        }
        frame = requestAnimationFrame(tick);
      };
      const move = (moveEvent: globalThis.PointerEvent) => {
        lastY = moveEvent.clientY;
      };
      const finish = (commit: boolean) => {
        window.removeEventListener("pointermove", move);
        window.removeEventListener("pointerup", release);
        window.removeEventListener("pointercancel", cancel);
        cancelAnimationFrame(frame);
        // The gap the track lands in; one less once the track has left a place above it.
        const landed = gap > from ? gap - 1 : gap;
        if (commit && from !== -1 && landed !== from) {
          moveTrack(id, landed);
        }
        setCopy(null);
        setDragId(null);
        setDrop(null);
      };
      const release = () => finish(true);
      const cancel = () => finish(false);

      const dragged = tracks.find((track) => track.id === id);
      setCopy({
        edge:
          TRACK_COLORS[mixOf(dragged ?? tracks[from] ?? tracks[0]).color]
            ?.edge ?? "",
        name: dragged?.name ?? "",
        top: box.top,
        left: header.left,
        width: header.width,
        height: box.height,
      });
      setDragId(id);
      window.addEventListener("pointermove", move);
      window.addEventListener("pointerup", release);
      window.addEventListener("pointercancel", cancel);
      frame = requestAnimationFrame(tick);
    },
    [mixOf, moveTrack, tracks]
  );
  const nudgeTrack = useCallback(
    (event: React.KeyboardEvent<HTMLButtonElement>) => {
      const id = event.currentTarget.dataset.trackId ?? "";
      const from = tracks.findIndex((track) => track.id === id);
      if (from === -1) {
        return;
      }
      if (event.key === "ArrowUp") {
        event.preventDefault();
        moveTrack(id, from - 1);
      } else if (event.key === "ArrowDown") {
        event.preventDefault();
        moveTrack(id, from + 1);
      }
    },
    [moveTrack, tracks]
  );
  const changeZoomPreset = useCallback(
    (value: string) => changeZoom(Number(value)),
    [changeZoom]
  );
  const handleDivisionValue = useCallback(
    (value: string) => setDivision(value as GridDivision),
    []
  );
  const seek = useCallback((seconds: number) => clock.set(seconds), [clock]);
  // A press on a track's empty lane puts the cursor there (the lane does it) and picks the track, letting go of the clips; on the surface under the last track it lets go of everything, as nothing is there.
  // The shortcuts are caught on the panel, so a press on the lane, which takes no focus, hands it over.
  const panelRef = useRef<HTMLDivElement>(null);
  const focusPanel = useCallback(
    (event: PointerEvent<HTMLDivElement>) => {
      if (event.target === event.currentTarget && !event.shiftKey && !multi) {
        const { track } = event.currentTarget.dataset;
        if (track) {
          selectTrack(track);
        } else {
          clearSelection();
        }
        // `focusVisible` is missing from older DOM typings.
        panelRef.current?.focus({
          focusVisible: false,
          preventScroll: true,
        } as FocusOptions);
      }
    },
    [clearSelection, multi, selectTrack]
  );
  const addLocator = useCallback(() => {
    const at = clock.getSnapshot();
    setLocators((all) =>
      all.some((locator) => locator.at === at)
        ? all
        : [...all, { at, name: `Locator ${all.length + 1}` }]
    );
  }, [clock]);
  const jumpToLocator = useCallback((at: number) => clock.set(at), [clock]);
  const removeLocator = useCallback(
    (at: number) =>
      setLocators((all) => all.filter((locator) => locator.at !== at)),
    []
  );
  const renameLocator = useCallback(
    (at: number, name: string) =>
      setLocators((all) =>
        all.map((locator) =>
          locator.at === at ? { ...locator, name } : locator
        )
      ),
    []
  );

  /** Puts the loop around the selected clip, or around the clips of the selected track. */
  const loopSelection = useCallback(() => {
    const chosen = clips.filter(
      (clip) =>
        selectedIds.includes(clip.id) || selectedTracks.includes(clip.track)
    );
    if (chosen.length === 0) {
      return;
    }
    setLoop([
      Math.min(...chosen.map((clip) => clip.value.start)),
      Math.max(...chosen.map((clip) => clip.value.start + clip.value.length)),
    ]);
    setLooping(true);
  }, [clips, selectedIds, selectedTracks]);

  /** M adds a locator, Alt + arrows jump between them; returns whether the key was one of these. */
  const handleLocatorKey = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      if (
        event.key.toLowerCase() === "m" &&
        !(event.metaKey || event.ctrlKey || event.altKey)
      ) {
        event.preventDefault();
        addLocator();
        return true;
      }
      const direction = { ArrowLeft: -1, ArrowRight: 1 }[event.key];
      if (!(event.altKey && direction)) {
        return false;
      }
      const target = adjacentPoint(
        locators.map((locator) => locator.at),
        clock.getSnapshot(),
        direction === 1 ? 1 : -1
      );
      if (target === null) {
        return false;
      }
      event.preventDefault();
      event.stopPropagation();
      clock.set(target);
      return true;
    },
    [addLocator, clock, locators]
  );
  // The copies are there to be seen: bring them into view once they are drawn.
  const duplicateAndShow = useCallback(() => {
    const copies = duplicateSelection();
    requestAnimationFrame(() => {
      document
        .querySelector(`[data-clip-id="${copies[0]?.id}"]`)
        ?.scrollIntoView({ block: "nearest", inline: "nearest" });
    });
  }, [duplicateSelection]);
  /** Cmd/Ctrl + A selects every clip, Cmd/Ctrl + D duplicates the selection; returns whether the key was one of these. */
  const handleSelectionKey = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      const key = event.key.toLowerCase();
      if (!(event.metaKey || event.ctrlKey)) {
        return false;
      }
      if (key === "z" || key === "y") {
        event.preventDefault();
        if (key === "y" || event.shiftKey) {
          redo();
        } else {
          undo();
        }
        return true;
      }
      if (key === "a") {
        event.preventDefault();
        selectAll();
        return true;
      }
      if (key === "d" && selectedIds.length > 0) {
        event.preventDefault();
        duplicateAndShow();
        return true;
      }
      if (key === "d" && selectedTracks.length > 0) {
        event.preventDefault();
        for (const id of selectedTracks) {
          duplicateTrack(id);
        }
        return true;
      }
      return false;
    },
    [
      duplicateAndShow,
      duplicateTrack,
      redo,
      selectAll,
      selectedIds,
      selectedTracks,
      undo,
    ]
  );
  /** Delete takes away the tracks picked by their headers (a clip handles its own Delete); returns whether the key was it. */
  const handleTrackKey = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      const del = event.key === "Delete" || event.key === "Backspace";
      if (!(del && selectedTracks.length > 0 && selectedIds.length === 0)) {
        return false;
      }
      event.preventDefault();
      removeTrack(selectedTracks[0] ?? "");
      return true;
    },
    [removeTrack, selectedIds.length, selectedTracks]
  );
  // S, or Cmd/Ctrl + E, splits at the cursor, Cmd/Ctrl + D duplicates the clip; Escape drops the selection.
  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      if (event.key === "Escape") {
        clearSelection();
        return;
      }
      const key = event.key.toLowerCase();
      if (handleTrackKey(event)) {
        return;
      }
      if (handleLocatorKey(event)) {
        return;
      }
      if (key === "l" && (event.metaKey || event.ctrlKey) && selection) {
        event.preventDefault();
        loopSelection();
        return;
      }
      if (handleSelectionKey(event)) {
        return;
      }
      const split =
        (key === "s" && !(event.metaKey || event.ctrlKey || event.altKey)) ||
        (key === "e" && (event.metaKey || event.ctrlKey));
      if (split && selection) {
        event.preventDefault();
        splitAtCursor();
      }
    },
    [
      clearSelection,
      handleTrackKey,
      handleLocatorKey,
      handleSelectionKey,
      loopSelection,
      selection,
      splitAtCursor,
    ]
  );

  // The grid the lanes draw is the grid everything snaps to.
  // Snap off draws no lines and snaps nothing: a step of 0 is no grid.
  const gridSeconds = gridInterval(division, zoom, tempo, { triplet });
  const snapStep = snap ? gridSeconds : 0;
  const labelEvery = barLabelInterval(zoom, tempo);
  // Whole bars, counted rather than divided, so no tempo leaves a label like 6.9999.
  const labelBars = Math.max(1, Math.round(labelEvery / bar));
  const labels = Array.from(
    { length: Math.ceil(BARS / labelBars) },
    (_, index) => index * labelBars
  );

  return (
    // biome-ignore lint/a11y/noStaticElementInteractions: the editor's shortcuts, caught from the controls inside it
    <div
      className="rounded-xl @container/panel flex w-full min-w-0 flex-col gap-4 border bg-card p-4 outline-none"
      onKeyDown={handleKeyDown}
      ref={panelRef}
      tabIndex={-1}
    >
      <div className="flex items-center justify-between gap-2 p-1">
        <div className="flex min-w-0 flex-1 items-center gap-2">
          <div className="@3xl/panel:hidden" ref={narrowProbeRef} />
          <Transport
            className="min-w-0 flex-1 flex-nowrap"
            looping={looping}
            metronome={metronome}
            onLoopingChange={setLooping}
            onMetronomeChange={setMetronome}
            onPlayPause={playPause}
            onRhythmChange={setRhythm}
            onStop={stop}
            onTempoChange={changeTempo}
            playing={playing}
            rhythm={rhythm}
            source={clock}
            tempo={tempo}
          />
          <div className="hidden h-7 w-10 shrink-0 items-center @3xl/panel:flex">
            <Meter
              aria-label="Master output level"
              className="data-[orientation=horizontal]:[--meter-thickness:--spacing(3)]"
              orientation="horizontal"
              peakMarks="hidden"
              size="sm"
              source={playing ? simulated : SILENCE}
              warnThreshold={-12}
            />
          </div>
        </div>
        <div className="flex min-w-0 items-center gap-2">
          <ButtonGroup className="hidden @3xl/panel:flex">
            <Button
              aria-label="Undo"
              disabled={!canUndo}
              onClick={undo}
              size="icon"
              variant="outline"
            >
              <IconPlaceholder
                hugeicons="Undo02Icon"
                lucide="Undo2Icon"
                phosphor="ArrowUUpLeftIcon"
                remixicon="RiArrowGoBackLine"
                tabler="IconArrowBackUp"
              />
            </Button>
            <Button
              aria-label="Redo"
              disabled={!canRedo}
              onClick={redo}
              size="icon"
              variant="outline"
            >
              <IconPlaceholder
                hugeicons="Redo02Icon"
                lucide="Redo2Icon"
                phosphor="ArrowUUpRightIcon"
                remixicon="RiArrowGoForwardLine"
                tabler="IconArrowForwardUp"
              />
            </Button>
          </ButtonGroup>
          <div
            className="hidden items-center gap-2 @7xl/panel:flex"
            ref={zoomSlotRef}
          >
            <NumberControl
              format={PERCENT}
              label="Zoom"
              largeStep={1}
              max={maxRatio}
              min={1}
              onValueChange={changeZoom}
              step={0.25}
              steppers={ZOOM_STEPPERS}
              unit="Zoom"
              value={zoomRatio}
            />
          </div>
          <DropdownMenu>
            <DropdownMenuTrigger
              render={
                <Button
                  aria-label="More controls"
                  size="icon"
                  variant="outline"
                />
              }
            >
              <IconPlaceholder
                hugeicons="MoreHorizontalIcon"
                lucide="EllipsisIcon"
                phosphor="DotsThreeIcon"
                remixicon="RiMoreLine"
                tabler="IconDots"
              />
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end" className="w-auto min-w-48">
              {narrow ? (
                <>
                  <DropdownMenuItem disabled={!canUndo} onClick={undo}>
                    Undo
                  </DropdownMenuItem>
                  <DropdownMenuItem disabled={!canRedo} onClick={redo}>
                    Redo
                  </DropdownMenuItem>
                  <DropdownMenuSeparator />
                </>
              ) : null}
              <DropdownMenuCheckboxItem
                checked={multi}
                onCheckedChange={setMulti}
              >
                Multi-select
              </DropdownMenuCheckboxItem>
              <DropdownMenuSeparator />
              <DropdownMenuSub>
                <DropdownMenuSubTrigger>
                  Keyboard shortcuts
                </DropdownMenuSubTrigger>
                <DropdownMenuSubContent className="flex max-h-(--available-height) w-auto flex-col">
                  <div className="scrollbar flex min-h-0 flex-col gap-2 overflow-y-auto p-2 text-sm whitespace-nowrap">
                    {SHORTCUTS.map((shortcut) => (
                      <div
                        className="flex items-center justify-between gap-3"
                        key={`${shortcut.action}-${shortcut.keys.join("+")}`}
                      >
                        <span>{shortcut.action}</span>
                        <KbdGroup>
                          {shortcut.keys.map((key) => (
                            <Kbd key={key}>{key}</Kbd>
                          ))}
                        </KbdGroup>
                      </div>
                    ))}
                  </div>
                </DropdownMenuSubContent>
              </DropdownMenuSub>
              {zoomInBar ? null : (
                <>
                  <DropdownMenuSeparator />
                  <DropdownMenuSub>
                    <DropdownMenuSubTrigger>Zoom</DropdownMenuSubTrigger>
                    <DropdownMenuSubContent className="w-auto">
                      <div className="p-2">
                        <NumberControl
                          format={PERCENT}
                          label="Zoom"
                          largeStep={1}
                          max={maxRatio}
                          min={1}
                          onValueChange={changeZoom}
                          step={0.25}
                          steppers={ZOOM_STEPPERS}
                          unit="Zoom"
                          value={zoomRatio}
                        />
                      </div>
                      <DropdownMenuSeparator />
                      <DropdownMenuRadioGroup
                        onValueChange={changeZoomPreset}
                        value={
                          zoomPreset === undefined ? "" : String(zoomPreset)
                        }
                      >
                        {ZOOM_PRESETS.map((ratio) => (
                          <DropdownMenuRadioItem
                            disabled={ratio > maxRatio}
                            key={ratio}
                            value={String(ratio)}
                          >
                            {ratio === 1 ? "Fit" : `${ratio * 100} %`}
                          </DropdownMenuRadioItem>
                        ))}
                      </DropdownMenuRadioGroup>
                    </DropdownMenuSubContent>
                  </DropdownMenuSub>
                </>
              )}
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
      </div>
      <Timeline
        className="min-h-0 flex-1 gap-2"
        duration={duration}
        maxZoom={MAX_ZOOM}
        minZoom={fitPx || undefined}
        onPositionChange={seek}
        onZoomChange={setZoom}
        snapStep={snapStep}
        source={clock}
        zoom={zoomed === null ? undefined : zoom}
      >
        <TimelineNavigator getAriaValueText={describe} />
        <TimelineViewport
          className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-muted/40"
          ref={viewportRef}
          scrollbar="hidden"
        >
          {/* As wide as the lanes, so the stuck headers stay put however far it scrolls. */}
          <div
            className={cn(
              "flex min-h-full w-max min-w-full shrink-0 flex-col",
              marquee.rect && "select-none"
            )}
            {...marquee.handlers}
          >
            {/* One sticky block, so what is stuck stays stacked whatever each row measures. */}
            <div className="sticky top-0 z-40 flex min-w-full shrink-0 flex-col bg-background">
              <Row
                className="shrink-0 border-foreground/25 border-b"
                lead={
                  <span className="flex h-full items-center px-3 text-xs text-muted-foreground">
                    Loop
                  </span>
                }
              >
                <TimelineLane className="h-6 cursor-cell border-b-0">
                  <TimelineRange
                    aria-label="Loop"
                    drawable
                    getAriaValueText={describe}
                    onValueChange={setLoop}
                    value={loop}
                  >
                    <TimelineRangeHandle aria-label="Loop start" edge="start" />
                    <TimelineRangeHandle aria-label="Loop end" edge="end" />
                  </TimelineRange>
                  <TimelinePlayhead />
                </TimelineLane>
              </Row>
              <Row
                className="shrink-0 border-foreground/25 border-b bg-background"
                lead={
                  <DropdownMenu>
                    <DropdownMenuTrigger
                      render={
                        <Button
                          aria-label="Grid"
                          className="h-full w-full justify-start rounded-none border-0 tabular-nums"
                          variant="ghost"
                        />
                      }
                    >
                      {snap ? gridLabel(gridSeconds, tempo) : "Snap off"}
                    </DropdownMenuTrigger>
                    <DropdownMenuContent
                      align="start"
                      className="w-auto min-w-48"
                    >
                      <TimelineGridMenuItems
                        division={division}
                        divisions={DIVISIONS}
                        onDivisionChange={handleDivisionValue}
                        onSnapChange={setSnap}
                        onTripletChange={setTriplet}
                        snap={snap}
                        triplet={triplet}
                        variant="dropdown"
                      />
                    </DropdownMenuContent>
                  </DropdownMenu>
                }
              >
                <TimelineRuler
                  aria-label="Playhead"
                  className="border-b-0"
                  getAriaValueText={describe}
                >
                  {labels.map((count) => (
                    <TimelineMark key={count} value={count * bar}>
                      {count + 1}
                    </TimelineMark>
                  ))}
                  <TimelinePlayhead />
                </TimelineRuler>
              </Row>
              <Row
                className="shrink-0 border-foreground/25 border-b bg-background"
                lead={
                  <span className="flex h-full items-center px-3 text-xs text-muted-foreground">
                    Markers
                  </span>
                }
              >
                <TimelineLane className="h-6 border-b-0">
                  {locators.map((locator) => (
                    <TimelineMark key={locator.at} value={locator.at}>
                      <LocatorButton
                        locator={locator}
                        onJump={jumpToLocator}
                        onRemove={removeLocator}
                        onRename={renameLocator}
                      />
                    </TimelineMark>
                  ))}
                </TimelineLane>
              </Row>
            </div>
            {tracks.map((track) => (
              <div
                aria-label={track.name}
                className="contents"
                data-track-id={track.id}
                key={track.id}
                role="group"
              >
                <Row
                  className={cn(
                    "border-foreground/25 border-b",
                    heights[track.id] === undefined && "flex-1",
                    // The track being moved stays in place, faded; the line marks the gap it would land in.
                    dragId === track.id && "opacity-50",
                    drop?.id === track.id &&
                      drop.edge === "top" &&
                      "shadow-[inset_0_2px_0_0_var(--primary)]",
                    drop?.id === track.id &&
                      drop.edge === "bottom" &&
                      "shadow-[inset_0_-2px_0_0_var(--primary)]"
                  )}
                  height={heights[track.id]}
                  lead={
                    <div className="flex min-w-0 flex-1">
                      <button
                        aria-label={`Move ${track.name}`}
                        className={cn(
                          "absolute inset-y-0 inset-s-1 z-10 flex w-3 cursor-grab items-center justify-center text-muted-foreground/60 transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:outline-none active:cursor-grabbing",
                          dragId === track.id &&
                            "cursor-grabbing bg-muted text-foreground"
                        )}
                        data-track-id={track.id}
                        onKeyDown={nudgeTrack}
                        onPointerDown={startTrackDrag}
                        type="button"
                      >
                        <svg
                          aria-hidden="true"
                          className="size-3.5"
                          fill="currentColor"
                          focusable="false"
                          viewBox="0 0 16 16"
                        >
                          <circle cx="5.5" cy="3.5" r="1.25" />
                          <circle cx="10.5" cy="3.5" r="1.25" />
                          <circle cx="5.5" cy="8" r="1.25" />
                          <circle cx="10.5" cy="8" r="1.25" />
                          <circle cx="5.5" cy="12.5" r="1.25" />
                          <circle cx="10.5" cy="12.5" r="1.25" />
                        </svg>
                      </button>
                      <TimelineTrackControls
                        compact={
                          heights[track.id] !== undefined &&
                          heights[track.id] < COMPACT_TRACK_HEIGHT
                        }
                        folded={folded[track.id]}
                        meter={
                          <TrackLevel
                            active={
                              playing &&
                              !mixOf(track).muted &&
                              (!anySolo || mixOf(track).soloed)
                            }
                            name={track.name}
                          />
                        }
                        mix={mixOf(track)}
                        name={track.name}
                        onDuplicate={duplicateTrack}
                        onInsert={addTrack}
                        onMix={changeMix}
                        onRemove={removeTrack}
                        onRename={renameTrack}
                        onSelect={pickTrack}
                        onToggleFold={toggleFold}
                        selected={selectedTracks.includes(track.id)}
                        trackId={track.id}
                      />
                    </div>
                  }
                  onResize={resizeTrack}
                  selected={selectedTracks.includes(track.id)}
                  trackId={track.id}
                >
                  <ContextMenu>
                    <ContextMenuTrigger render={<div className="contents" />}>
                      <TimelineLane
                        className={cn(
                          "h-auto flex-1 border-b-0",
                          heights[track.id] === undefined
                            ? "min-h-14"
                            : "min-h-0",
                          // A short track keeps its clips thin, so they fit.
                          heights[track.id] !== undefined &&
                            heights[track.id] < COMPACT_TRACK_HEIGHT &&
                            "**:data-clip-id:inset-y-0.5",
                          selectedTracks.includes(track.id) && "bg-muted/40",
                          // A silent track keeps its clips on the page, drained.
                          (mixOf(track).muted ||
                            (anySolo && !mixOf(track).soloed)) &&
                            "**:data-clip-id:opacity-40"
                        )}
                        data-track={track.id}
                        gridStep={snapStep}
                        onPointerDown={focusPanel}
                        seekable
                      >
                        {copying
                          .filter((original) => original.track === track.id)
                          .map((original) => (
                            <CopyGhost clip={original} key={original.id} />
                          ))}
                        {clips
                          .filter((clip) => clip.track === track.id)
                          .map((clip) => (
                            <ClipView
                              clip={clip}
                              copying={copying.some(
                                (original) => original.id === clip.id
                              )}
                              describe={describe}
                              key={clip.id}
                              lift={
                                lifting &&
                                lifting.id !== clip.id &&
                                selectedIds.includes(clip.id) &&
                                selectedIds.includes(lifting.id)
                                  ? lifting.offset
                                  : 0
                              }
                              multi={multi}
                              onAction={clipAction}
                              onChange={changeClip}
                              onCommit={commitClip}
                              onFadeShape={setFadeShape}
                              onLift={lift}
                              onRemove={removeClip}
                              onRename={renameClip}
                              onSelect={selectClip}
                              selected={selectedIds.includes(clip.id)}
                              swatch={mixOf(track).color}
                              trackName={track.name}
                            />
                          ))}
                        <TimelinePlayhead />
                      </TimelineLane>
                    </ContextMenuTrigger>
                    <ContextMenuContent className="w-auto min-w-48">
                      <TimelineGridMenuItems
                        division={division}
                        divisions={DIVISIONS}
                        onDivisionChange={handleDivisionValue}
                        onSnapChange={setSnap}
                        onTripletChange={setTriplet}
                        snap={snap}
                        triplet={triplet}
                      />
                    </ContextMenuContent>
                  </ContextMenu>
                </Row>
              </div>
            ))}
            <Row
              className="sticky bottom-0 z-30 border-foreground/25 border-b bg-background"
              lead={
                <Button
                  className="m-1 min-w-0 flex-1 justify-start gap-1.5"
                  onClick={handleAddTrack}
                  size="sm"
                  variant="ghost"
                >
                  <IconPlaceholder
                    hugeicons="PlusSignIcon"
                    lucide="PlusIcon"
                    phosphor="PlusIcon"
                    remixicon="RiAddLine"
                    tabler="IconPlus"
                  />
                  Add track
                </Button>
              }
            >
              <TimelineLane
                className="h-10 border-b-0 bg-background"
                gridStep={snapStep}
              />
            </Row>
            {/* The surface carries on to the bottom of the view, so the arrangement does not stop at the last track. */}
            <Row className="flex-1" lead={<div className="h-full" />}>
              <TimelineLane
                className="h-auto flex-1 border-b-0 bg-background"
                gridStep={snapStep}
                onPointerDown={focusPanel}
                seekable
              >
                <TimelinePlayhead />
              </TimelineLane>
            </Row>
          </div>
        </TimelineViewport>
        {marquee.rect
          ? // In the page's body: a dialog is transformed, which would make a fixed rectangle its own.
            createPortal(
              <div
                aria-hidden="true"
                className="pointer-events-none fixed z-50 border border-primary bg-primary/10"
                style={marquee.rect}
              />,
              document.body
            )
          : null}
      </Timeline>
      {copy
        ? createPortal(
            <div
              aria-hidden="true"
              className={cn(
                "pointer-events-none fixed z-50 flex items-center border border-s-4 bg-card px-3 text-sm shadow-lg",
                copy.edge
              )}
              ref={copyRef}
              style={{
                height: copy.height,
                left: copy.left,
                top: copy.top,
                width: copy.width,
              }}
            >
              {copy.name}
            </div>,
            document.body
          )
        : null}
    </div>
  );
}
```
