# Audio Scrub Player

Audio Scrub Player [#audio-scrub-player]

**Audio Scrub Player** is a composable scrubbable player for local `Blob` / object-URL audio such as Kokoro TTS output ([`useSynthesizeSpeech`](https://localmode.dev/docs/react)) or a recording. It provides play/pause, a draggable seek bar, and a time/duration readout, and manages its own `<audio>` element and object-URL lifecycle. The draggable `ScrubBar` is exported standalone for reuse over any time-based source.

**When to use it:** play back a locally-synthesized or recorded clip with precise scrubbing — without the inconsistent look of native `<audio controls>`.

Preview [#preview]

```tsx
'use client';

import { useMemo } from 'react';
import { AudioScrubPlayer } from '@/components/audio-scrub-player';

/**
 * Demo for {@link AudioScrubPlayer}. Plays a generated 3-second tone WAV so
 * play/pause, scrubbing, and the duration readout work against a real audio
 * element without a model download. The real app passes a Kokoro
 * `useSynthesizeSpeech` Blob or a recording.
 */
function makeToneWav(seconds: number, freq: number): Blob {
  const sampleRate = 22050;
  const samples = Math.floor(sampleRate * seconds);
  const buffer = new ArrayBuffer(44 + samples * 2);
  const view = new DataView(buffer);
  const writeStr = (offset: number, s: string) => {
    for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i));
  };
  writeStr(0, 'RIFF');
  view.setUint32(4, 36 + samples * 2, true);
  writeStr(8, 'WAVE');
  writeStr(12, 'fmt ');
  view.setUint32(16, 16, true);
  view.setUint16(20, 1, true);
  view.setUint16(22, 1, true);
  view.setUint32(24, sampleRate, true);
  view.setUint32(28, sampleRate * 2, true);
  view.setUint16(32, 2, true);
  view.setUint16(34, 16, true);
  writeStr(36, 'data');
  view.setUint32(40, samples * 2, true);
  for (let i = 0; i < samples; i++) {
    const v = Math.sin((2 * Math.PI * freq * i) / sampleRate) * 0.25;
    view.setInt16(44 + i * 2, v * 0x7fff, true);
  }
  return new Blob([buffer], { type: 'audio/wav' });
}

export default function AudioScrubPlayerDemo() {
  const blob = useMemo(() => makeToneWav(3, 440), []);
  return (
    <div className="w-full max-w-md">
      <AudioScrubPlayer audio={blob} />
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/audio/audio-scrub-player
```

Data source & dependencies [#data-source--dependencies]

**Data source:** plays any `Blob` / object-URL you pass — works with any backend. Recommended producer: `useSynthesizeSpeech` output (local Blob) from `@localmode/react` (on-device, optional).

* `@localmode/ui/lib/browser-utils` — `useObjectUrl` for Blob lifecycle (installed automatically as a registry dependency)
* `clsx` + `tailwind-merge` — via the shared `cn()` util
* No external player dependency — native `<audio>` + custom controls

Files installed [#files-installed]

* `audio-scrub-player.tsx` — `AudioScrubPlayer` + `ScrubBar`
* `lib/browser-utils.ts` — generic browser helpers (`useObjectUrl`)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**AudioScrubPlayer**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `audio` | `object \| string` | — | **Required.** Local audio to play — a `Blob` (e.g. Kokoro output / a recording) or URL. |
| `autoPlay` | `boolean` | `false` | Auto-play once loaded. |

**ScrubBar**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `currentTime` | `number` | — | **Required.** Current playback position in seconds. |
| `duration` | `number` | — | **Required.** Total duration in seconds. |
| `onSeek` | `function` | — | **Required.** Fired continuously while dragging, and on click, with the new time. |
| `onSeekEnd` | `function` | — | Fired once when a drag gesture ends (useful to commit a seek). |

Examples [#examples]

Play Kokoro output [#play-kokoro-output]

```tsx
import { AudioScrubPlayer } from '@/components/audio-scrub-player';
import { useSynthesizeSpeech } from '@localmode/react';
import { transformers } from '@localmode/transformers';

export function Speak({ text }: { text: string }) {
  const { data, execute } = useSynthesizeSpeech({
    model: transformers.textToSpeech('onnx-community/Kokoro-82M-v1.0-ONNX'),
  });

  return (
    <>
      <button onClick={() => execute(text)}>Synthesize</button>
      {data && <AudioScrubPlayer audio={data.audio} />}
    </>
  );
}
```

Use the standalone ScrubBar [#use-the-standalone-scrubbar]

```tsx
import { ScrubBar } from '@/components/audio-scrub-player';

<ScrubBar currentTime={time} duration={duration} onSeek={setTime} />
```

Customization [#customization]

`AudioScrubPlayer` owns its `<audio>` and reads `duration` / `currentTime` from the media events, so it works with any decodable Blob or URL. `ScrubBar` is controlled (pointer + keyboard, with `ArrowLeft`/`ArrowRight` for ±5s) — drop it into your own player shell if you need a different layout. The progress fill uses `bg-primary`; the thumb uses `border-primary` / `bg-background`.