# Voice Dictation

> Mic button that expands into a live waveform pill with a timer and live transcript; speech-to-text with recording fallback, toggle or hold-to-talk.

- **Slug:** `voice-dictation` · **Category:** voice · **Status:** experimental
- **Import:** `import { VoiceDictation, VoiceDictationStatus } from "@kura-ui/react/voice-dictation";`
- **Docs:** https://kuro.abstergo.fyi/components/voice-dictation
- **Registry item:** https://kuro.abstergo.fyi/r/voice-dictation.json
- **Tags:** voice, dictation, speech-to-text, microphone, push-to-talk, transcription, waveform

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/voice-dictation.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add voice-dictation
# npm package
npm install @kura-ui/react
```

## When to use

- Let users dictate a prompt instead of typing — standalone or in a composer toolbar.
- Push-to-talk input (mode="hold") for voice notes or quick commands.
- Browsers without the Web Speech API: record audio and transcribe on your server via `transcribe`.

## When not to use

- A full-duplex conversation with a voice agent (barge-in, spoken replies) — use agent-orb with your realtime session.
- Only visualising audio you already have — use audio-waveform.
- Typing prompts — use composer.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `onTranscript` | `(text: string) => void` | — | no | Called once per dictation with the final trimmed text. Not called when cancelled (× or Escape) or when nothing was heard. |
| `onInterim` | `(text: string) => void` | — | no | Live text (final + interim) while listening, e.g. to preview in a placeholder. |
| `onStatusChange` | `(status: VoiceDictationStatus) => void` | — | no | idle \| requesting \| listening \| processing \| denied \| unsupported \| error. |
| `mode` | `"toggle" \| "hold"` | `"toggle"` | no | toggle = click to start/stop; hold = dictate while the pointer, Space/Enter or the hotkey is held. |
| `hotkey` | `readonly KeyName[] \| null` | `["mod", "shift", "d"]` | no | Global shortcut (works while typing). null disables it. |
| `lang` | `string` | — | no | BCP-47 language for recognition, e.g. "en-US". Defaults to the page language. |
| `transcribe` | `(audio: Blob) => Promise<string>` | — | no | Fallback transcription when the Web Speech API is missing (Firefox): receives the recording; status is “processing” until it resolves. |
| `simulate` | `boolean \| string` | — | no | Demo mode: fake permission, simulated waveform and a scripted transcript (a string is typed out word by word). For docs and tests. |
| `showTranscript` | `boolean` | `true` | no | Show the live transcript card under the control. Turn off inside a composer. |
| `transcriptPlaceholder` | `string` | `"Listening…"` | no | Text in the transcript card before words arrive. |
| `disabled` | `boolean` | `false` | no | Disable the control. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **VoiceDictation** (`data-slot="voice-dictation"`) — Root column. data-status (see onStatusChange) and data-mode.
- **VoiceDictationControl** (`data-slot="voice-dictation-control"`) — The mic button; while live it grows (layout spring) into a signal-tinted pill with waveform, timer and cancel.
- **VoiceDictationButton** (`data-slot="voice-dictation-button"`) — Mic / stop / mic-off button with aria-pressed and a tooltip showing the hotkey.
- **VoiceDictationCancel** (`data-slot="voice-dictation-cancel"`) — × button that discards the dictation.
- **VoiceDictationTranscript** (`data-slot="voice-dictation-transcript"`) — Live transcript: final words in ink, interim words subtle, a signal caret while listening.
- **VoiceDictationMessage** (`data-slot="voice-dictation-message"`) — Recovery text for denied / unsupported / error.

## Accessibility

- The button is labelled “Dictate” (or “Hold to dictate”) with aria-pressed reflecting listening; the stop/mic icon swap is visual only.
- A visually hidden role=status announces state changes once (“Listening”, “Transcribing”, “Microphone access is blocked”) — never per word.
- Denied and unsupported states keep a visible text explanation and the button stays reachable to retry.
- The waveform is decorative (aria-hidden); the timer is plain text with tabular numbers.
- Reduced motion: The pill appears without the layout spring, icon swaps fade without scale or blur, the waveform becomes a slow level meter and the caret stops blinking.

| Keys | Action |
| --- | --- |
| Enter / Space | Toggle dictation (hold them in hold mode) |
| Mod+Shift+D | Global toggle / hold (configurable via hotkey) |
| Escape | Cancel and discard while listening |

## Notes for agents

- In docs, demos and tests pass `simulate` — real recognition needs a user gesture, mic permission and (for Chrome) a network connection.
- Feed a Composer by controlling its value: onTranscript={(t) => setValue((v) => (v ? v + ' ' + t : t))}; pass showTranscript={false}.
- Provide `transcribe` for Firefox/other browsers without SpeechRecognition; without it those browsers record but emit nothing.
- Only one mounted instance should own a given hotkey; pass hotkey={null} on the others.
- The listening pill uses the jade signal — avoid another accent-filled element in the same view while dictating.

## Examples

### Toggle with live transcript (minimal)

```tsx
"use client";

import { VoiceDictation } from "@kura-ui/react/voice-dictation";

export function VoiceDictationMinimal() {
  return (
    <div className="w-full max-w-md">
      <VoiceDictation simulate onTranscript={(text) => console.log(text)} />
    </div>
  );
}
```

### Toggle or hold, hotkey and collected notes (full)

Switch modes, use ⌘⇧D, and watch final transcripts collect below.

```tsx
"use client";

import * as React from "react";
import { KbdShortcut } from "@kura-ui/react/kbd-shortcut";
import { VoiceDictation, type VoiceDictationStatus } from "@kura-ui/react/voice-dictation";

export function VoiceDictationFull() {
  const [notes, setNotes] = React.useState<string[]>([]);
  const [status, setStatus] = React.useState<VoiceDictationStatus>("idle");
  const [mode, setMode] = React.useState<"toggle" | "hold">("toggle");
  return (
    <div className="flex w-full max-w-md flex-col gap-4">
      <div className="flex items-center justify-between gap-3">
        <div className="flex rounded-kura-md bg-kura-surface-sunken p-0.5 text-xs shadow-kura-xs">
          {(["toggle", "hold"] as const).map((m) => (
            <button
              key={m}
              type="button"
              aria-pressed={mode === m}
              onClick={() => setMode(m)}
              className="cursor-pointer rounded-kura-sm px-2.5 py-1 text-kura-fg-muted capitalize outline-none transition-colors duration-(--kura-duration-fast) focus-visible:shadow-kura-focus aria-pressed:bg-kura-surface aria-pressed:text-kura-fg aria-pressed:shadow-kura-xs"
            >
              {m}
            </button>
          ))}
        </div>
        <KbdShortcut keys={["mod", "shift", "d"]} label={mode === "hold" ? "Hold" : "Toggle"} />
      </div>
      <VoiceDictation
        simulate="Remind me to send Priya the revised contract after the design review tomorrow."
        mode={mode}
        hotkey={["mod", "shift", "d"]}
        lang="en-US"
        showTranscript
        transcriptPlaceholder="Speak — your words appear here"
        onStatusChange={setStatus}
        onTranscript={(text) => setNotes((n) => [text, ...n])}
      />
      <p className="text-kura-fg-subtle text-xs capitalize" aria-live="polite">
        Status: {status}
      </p>
      <ul aria-label="Dictated notes" className="flex flex-col gap-2">
        {notes.length === 0 && (
          <li className="rounded-kura-lg bg-kura-surface-sunken px-3.5 py-3 text-kura-fg-subtle text-sm">
            Dictated notes appear here.
          </li>
        )}
        {notes.map((note, i) => (
          <li
            key={`${i}-${note}`}
            className="animate-kura-rise-in rounded-kura-lg bg-kura-surface px-3.5 py-3 text-kura-fg text-sm leading-6 shadow-kura-sm"
          >
            {note}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

### Dictating into a composer (composition)

Interim words preview in the placeholder; the final transcript is appended to the composer value.

```tsx
"use client";

import * as React from "react";
import {
  Composer,
  ComposerActions,
  ComposerAttachButton,
  ComposerInput,
  ComposerSubmit,
  ComposerToolbar,
  ComposerTools,
} from "@kura-ui/react/composer";
import { VoiceDictation } from "@kura-ui/react/voice-dictation";

export function VoiceDictationInComposer() {
  const [value, setValue] = React.useState("");
  const [interim, setInterim] = React.useState("");
  return (
    <div className="w-full max-w-xl">
      <Composer value={value} onValueChange={setValue} onSubmit={() => setValue("")}>
        <ComposerInput placeholder={interim || "Type or dictate…"} />
        <ComposerToolbar>
          <ComposerTools>
            <ComposerAttachButton />
          </ComposerTools>
          <ComposerActions>
            <VoiceDictation
              simulate="Find three cafés near the office that open before seven."
              showTranscript={false}
              hotkey={null}
              onInterim={setInterim}
              onStatusChange={(s) => s === "idle" && setInterim("")}
              onTranscript={(text) => setValue((v) => (v ? `${v.trimEnd()} ${text}` : text))}
            />
            <ComposerSubmit />
          </ComposerActions>
        </ComposerToolbar>
      </Composer>
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`, `motion`
- Kura registry items: `kura-theme`, `audio-waveform`, `kura-keys`, `kura-motion`, `shimmer-text`, `spinner`, `tooltip`, `use-audio-analyser`, `use-hotkey`, `use-media-recorder`, `use-reduced-motion`, `use-speech-recognition`, `utils`
- Related: `composer`, `audio-waveform`, `agent-orb`
