# Sequencer

> A step grid: press a step, or drag across several, to draw a pattern.

```tsx
"use client";

import { createSource } from "@audio-ui/react";
import { useCallback, useEffect, useRef, useState } from "react";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { Button } from "@/components/ui/button";
import { Sequencer } from "@/components/audio-ui/sequencer";
import { useMasterBus } from "@/hooks/use-master-bus";
import { useTransportClock } from "@/hooks/use-transport-clock";
import {
  triggerHihat,
  triggerKick,
  triggerSnare,
} from "@/lib/drum-synthesis";
import { webAudio } from "@/lib/web-audio";

const STEPS = 16;
const STEPS_PER_BEAT = 4;
const BPM = 112;
const VOLUME = 0.7;
const ROWS = ["Kick", "Snare", "Hihat"];
const TRIGGERS = [triggerKick, triggerSnare, triggerHihat];

const VELOCITIES = [
  [1, 0.5, 0.7, 0.5, 0.9, 0.5, 0.7, 0.5, 1, 0.5, 0.7, 0.5, 0.9, 0.5, 0.7, 0.6],
  [1, 1, 1, 1, 0.8, 1, 1, 1, 1, 1, 1, 1, 0.85, 1, 1, 0.4],
  [
    0.55, 0.6, 0.9, 0.6, 0.55, 0.6, 0.9, 0.6, 0.55, 0.6, 0.9, 0.6, 0.55, 0.6,
    0.9, 0.6,
  ],
];

const x = (steps: number[]) =>
  Array.from({ length: STEPS }, (_, step) => steps.includes(step));
const BEAT = [x([0, 4, 8, 12]), x([4, 12]), x([2, 6, 10, 14])];

/** A drum machine: draw a pattern and hear it. Press a step, or drag across several; the playing column lights up. */
export function SequencerDemo() {
  const [pattern, setPattern] = useState(BEAT);
  const patternRef = useRef(pattern);
  patternRef.current = pattern;
  const [velocities, setVelocities] = useState(VELOCITIES);
  const velocitiesRef = useRef(velocities);
  velocitiesRef.current = velocities;
  const [step] = useState(() => createSource(-1));
  const master = useMasterBus(VOLUME);

  const playStep = useCallback(
    (index: number, time: number) => {
      const ctx = webAudio.getContext();
      if (!(ctx && master)) {
        return;
      }
      patternRef.current.forEach((row, at) => {
        if (!row[index]) {
          return;
        }
        // Each hit goes through a gain of its own: the step's velocity.
        const hit = ctx.createGain();
        hit.gain.value = velocitiesRef.current[at]?.[index] ?? 1;
        hit.connect(master);
        TRIGGERS[at]?.(ctx, hit, time);
      });
    },
    [master]
  );
  const { getPosition, isPlaying, start, stop } = useTransportClock({
    bpm: BPM,
    onStep: playStep,
    stepsPerBeat: STEPS_PER_BEAT,
    totalSteps: STEPS,
  });

  useEffect(() => {
    if (!isPlaying) {
      step.set(-1);
      return;
    }
    let frame = 0;
    const tick = () => {
      step.set(Math.floor(getPosition()) % STEPS);
      frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [getPosition, isPlaying, step]);

  const toggle = useCallback(
    () => (isPlaying ? stop() : start()),
    [isPlaying, start, stop]
  );
  const clear = useCallback(
    () =>
      setPattern(ROWS.map(() => Array.from({ length: STEPS }, () => false))),
    []
  );

  return (
    <div className="flex w-full flex-col gap-3">
      <Sequencer
        aria-label="Drum pattern"
        onValueChange={setPattern}
        onVelocityChange={setVelocities}
        rowLabels={ROWS}
        showVelocity
        source={step}
        value={pattern}
        velocities={velocities}
      />
      <div className="flex items-center justify-between gap-2">
        <Button
          aria-label={isPlaying ? "Stop" : "Play"}
          onClick={toggle}
          size="icon-sm"
          variant="outline"
        >
          {isPlaying ? (
            <IconPlaceholder
              hugeicons="PauseIcon"
              lucide="PauseIcon"
              phosphor="PauseIcon"
              remixicon="RiPauseLine"
              tabler="IconPlayerPause"
            />
          ) : (
            <IconPlaceholder
              hugeicons="PlayIcon"
              lucide="PlayIcon"
              phosphor="PlayIcon"
              remixicon="RiPlayLine"
              tabler="IconPlayerPlay"
            />
          )}
        </Button>
        <Button onClick={clear} size="sm" variant="ghost">
          Clear
        </Button>
      </div>
    </div>
  );
}
```

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/sequencer
```

**Manual**

**1. Install the following dependencies:**

```bash
npm install @audio-ui/react
```

**2. Copy and paste the following code into your project.**

```tsx title="components/audio-ui/sequencer.tsx"
"use client";

import { Sequencer as SequencerPrimitive } from "@audio-ui/react";
import { cn } from "@/lib/utils";

type SequencerProps = Omit<SequencerPrimitive.Root.Props, "className"> & {
  className?: string;
  /** Names each row, such as "Kick": it labels the row for assistive technology and is written beside it. */
  rowLabels?: readonly string[];
  /** Highlights every `beatEvery` steps, such as 4 for a beat in sixteenths. */
  beatEvery?: number;
  /** Draws a bar under each row to set how loud every step plays: press or drag across it; Alt + click returns a step to full. */
  showVelocity?: boolean;
};

/**
 * A step sequencer: a row of steps per sound. Press a step to flip it and
 * drag on to paint the same state across the others; the arrow keys move
 * between steps and Space flips one. Pass `source` with the step being played
 * and its column lights up. With `showVelocity`, a bar under each row sets
 * how loud each step plays; pass `velocities` and `onVelocityChange` to follow
 * them.
 */
function Sequencer({
  beatEvery = 4,
  className,
  rowLabels,
  showVelocity = false,
  ...props
}: SequencerProps) {
  return (
    <SequencerPrimitive.Root
      className={cn(
        "[--sequencer-off:var(--muted)] w-full min-w-0 select-none data-disabled:opacity-50",
        // Labelled rows share one label column, as wide as the longest label.
        rowLabels
          ? "grid grid-cols-[auto_minmax(0,1fr)] gap-x-2 gap-y-1.5"
          : "flex flex-col gap-1.5",
        className
      )}
      data-slot="sequencer"
      {...props}
    >
      <SequencerPrimitive.Rows>
        {(row) => (
          <SequencerPrimitive.Row
            aria-label={rowLabels?.[row] ?? `Row ${row + 1}`}
            className={
              rowLabels
                ? "col-span-2 grid grid-cols-subgrid items-center"
                : "flex items-center"
            }
            index={row}
          >
            {rowLabels ? (
              <span
                aria-hidden="true"
                className="max-w-24 truncate text-muted-foreground text-xs"
              >
                {rowLabels[row]}
              </span>
            ) : null}
            <div className="flex min-w-0 flex-1 flex-col gap-1">
              <div className="grid grid-flow-col auto-cols-fr gap-1">
                <SequencerPrimitive.Cells>
                  {(step) => (
                    <SequencerPrimitive.Cell
                      aria-label={`${rowLabels?.[row] ?? `Row ${row + 1}`}, step ${step + 1}`}
                      className={cn(
                        "rounded-[min(var(--radius-sm),6px)] border bg-muted/60 transition-[background-color,box-shadow,transform] duration-100 ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-95 aria-pressed:border-primary aria-pressed:bg-primary data-current:ring-2 data-current:ring-foreground/40 data-disabled:pointer-events-none motion-reduce:transition-none motion-reduce:active:scale-100 h-9 min-w-0 cursor-pointer touch-none outline-none",
                        step % beatEvery === 0 && "bg-muted"
                      )}
                      step={step}
                    />
                  )}
                </SequencerPrimitive.Cells>
              </div>
              {showVelocity ? (
                <div className="grid grid-flow-col auto-cols-fr gap-1">
                  <SequencerPrimitive.Cells>
                    {(step) => (
                      <SequencerPrimitive.Velocity
                        aria-label={`${rowLabels?.[row] ?? `Row ${row + 1}`}, step ${step + 1} velocity`}
                        className="overflow-hidden rounded-[min(var(--radius-sm),6px)] bg-muted/40 focus-within:ring-2 focus-within:ring-ring/50 data-disabled:pointer-events-none relative h-8 min-w-0 cursor-ns-resize touch-none outline-none"
                        step={step}
                      >
                        <span className="bg-primary absolute inset-x-0 bottom-0 h-[calc(var(--sequencer-velocity)*100%)]" />
                      </SequencerPrimitive.Velocity>
                    )}
                  </SequencerPrimitive.Cells>
                </div>
              ) : null}
            </div>
          </SequencerPrimitive.Row>
        )}
      </SequencerPrimitive.Rows>
    </SequencerPrimitive.Root>
  );
}

export { Sequencer };
```

**3. Update the import paths to match your project setup.**

## Anatomy

Import the registry component for the ready-made look, or compose the primitive's parts yourself.

```tsx
import { Sequencer } from "@audio-ui/react";

<Sequencer.Root>
  <Sequencer.Rows>
    {(row) => (
      <Sequencer.Row index={row}>
        <Sequencer.Cells>
          {(step) => <Sequencer.Cell step={step} />}
        </Sequencer.Cells>
      </Sequencer.Row>
    )}
  </Sequencer.Rows>
</Sequencer.Root>;
```

## Usage

```tsx
import { Sequencer } from "@/components/audio-ui/sequencer";

<Sequencer
  aria-label="Drum pattern"
  onValueChange={setPattern}
  rowLabels={["Kick", "Snare", "Hihat"]}
  source={step}
  value={pattern}
/>;
```

`value` is a row per sound, each a list of steps: `boolean[][]`. `velocities` is how loud each step plays, from 0 to 1, in the same shape; a step it does not list plays at full. Add `showVelocity` for a bar under each row to draw them. `source` is the step being played, a live `{ getSnapshot, subscribe }` such as one made with `createSource`: a clock ticking every step never re-renders React.

## Interaction

**Gestures**

| Gesture | Effect |
| --- | --- |
| Press a step | Flips it. |
| Drag on | Paints the new state across every step the pointer crosses. |
| Arrow keys, `Home`, `End` | Move between steps; with `Ctrl` or `Cmd`, `Home` and `End` go to the corners. |
| `Space`, `Enter` | Flip the focused step. |
| Press or drag on a velocity bar | Sets the velocity where the pointer is, and draws across the steps of the row. |
| `Alt` + click, `Delete` on a velocity bar | Bring the step back to full. |
| Arrow keys, `Page Up`/`Down`, `Home`/`End` on a velocity bar | Change the velocity by 5%, by 25%, or to silent or full. |

The steps share one tab stop, and each is a toggle button (`aria-pressed`) named after its row and step.

## API reference

### Root

A grid of steps to switch on and off, one row per sound: the pattern of a drum machine. Pressing a cell flips it, and dragging on carries the new state across every cell the pointer crosses. The cells are one tab stop, and the arrow keys move between them. The step being played comes from `source` and is marked on its cells straight in the DOM with `data-current`, so a clock ticking every step never re-renders React. Renders a `<div>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultValue` | `Pattern` |  | Uncontrolled starting pattern: a row per sound, each a list of steps. |
| `defaultVelocities` | `Velocities` |  | Uncontrolled starting velocities. |
| `disabled` | `boolean` |  |  |
| `onValueChange` | `(value: boolean[][], eventDetails: ChangeEventDetails) => void` |  | Fires while cells are flipped. `eventDetails.cancel()` keeps the pattern as it was. |
| `onValueCommitted` | `(value: boolean[][], eventDetails: CommitEventDetails) => void` |  | Fires when a press, a paint or a key press that changed the pattern ends. |
| `onVelocityChange` | `(velocities: number[][], eventDetails: VelocityChangeEventDetails) => void` |  | Fires while a velocity is drawn or nudged. `eventDetails.cancel()` keeps the velocities as they were. |
| `onVelocityCommitted` | `(velocities: number[][], eventDetails: VelocityCommitEventDetails) => void` |  | Fires when a drag, a key press or a reset that changed a velocity ends. |
| `source` | `Source<number>` |  | The step being played, a live value followed without re-rendering; its cells get `data-current`. |
| `value` | `Pattern` |  | The pattern: a row per sound, each a list of steps. |
| `velocities` | `Velocities` |  | How loud each step plays, 0…1, a row per sound like the pattern; a step it does not list plays at full. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |
| `data-painting` | Whether a pointer is painting cells. |

### Rows

Renders `children` once per row: the place to put a `Sequencer.Row`. Renders no element of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `(row: number) => React.ReactNode` |  |  |

### Cells

Renders `children` once per step of the row it is in: the place to put a `Sequencer.Cell`. Renders no element of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `(step: number) => React.ReactNode` |  |  |

### Row

One sound's row of steps. Name it with `aria-label`, such as "Kick". Renders a `<div>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `index` | `number` |  | Which row this is, from the top. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |
| `data-painting` | Whether a pointer is painting cells. |

### Cell

One step of a row: a toggle button, pressed when the step is on. Press it and drag on to paint the same state across the cells you cross; focused, Space or Enter flips it and the arrow keys move to the next cell. Name it with `aria-label`, such as "Kick, step 5". The step being played gets `data-current`. Renders a `<button>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `step` | `number` |  | Which step this is, from the left. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |
| `data-on` | Whether the step is on: also `aria-pressed`. |
| `data-painting` | Present when painting. |

### Velocity

How loud one step plays, as a bar from the bottom of its box: the height is the velocity, through the `--sequencer-velocity` CSS variable. Pressing it sets the velocity where the pointer is, and dragging on draws across the steps of the row. Alt + click or Delete brings it back to full. A native `<input type="range">` inside takes the arrow keys and carries the accessible name. Put one per step of a `Sequencer.Row`, in a row of its own under the cells. Renders a `<div>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `step` | `number` |  | Which step this is, from the left. |
| `getAriaValueText` | `(velocity: number) => string` |  | Returns how a velocity, 0…1, is announced. Defaults to a percentage. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |
| `data-on` | Whether the step is on. |

### Registry component

`Sequencer` takes the `Root` props, plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rowLabels` | `string[]` |  | Names written beside each row, and its accessible name. |
| `beatEvery` | `number` | `4` | Highlights every nth step. |
| `showVelocity` | `boolean` | `false` | Draws a bar under each row to set how loud every step plays. |
