> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice Mode Fab – Code

> The VoiceModeFab is the floating control for starting, monitoring, and ending Atlas voice mode.

<Tabs>
  <Tab title="Implementation">
    ```tsx theme={null}
    import { VoiceModeFab } from "@servicetitan/anvil2-rn";

    function App() {
      return <VoiceModeFab state="idle" onPress={startVoice} />;
    }
    ```

    ## Common Examples

    The component has three states, discriminated on `state`. The host owns all voice state and transport; `VoiceModeFab` renders the control and reports presses.

    ### Collapsed

    `idle` is a plain dark circle — voice is available but not running. `active` is the same circle with a blue presence glow, meaning Atlas is listening. Both take a single `onPress`.

    ```tsx theme={null}
    <VoiceModeFab state="idle" onPress={startVoice} />
    <VoiceModeFab state="active" onPress={expand} />
    ```

    ### Expanded

    The control strip: a status `label` plus mic-mute, stop, and collapse. Those three are required — each appears in every frame of the design, and an absent handler would render a control that looks live and does nothing. A speaker control is available on top, and renders only when the `isSpeakerMuted` / `onToggleSpeakerMuted` pair is wired — they are optional as a unit, never half-set.

    ```tsx theme={null}
    <VoiceModeFab
      state="expanded"
      label="Talking"
      isMicMuted={micMuted}
      onToggleMicMuted={() => setMicMuted((m) => !m)}
      onStop={endVoice}
      onCollapse={() => setState("active")}
    />
    ```

    Wiring `onToggleSpeakerMuted` adds the Atlas-audio control:

    ```tsx theme={null}
    <VoiceModeFab
      state="expanded"
      label="Talking"
      isMicMuted={micMuted}
      onToggleMicMuted={() => setMicMuted((m) => !m)}
      isSpeakerMuted={speakerMuted}
      onToggleSpeakerMuted={() => setSpeakerMuted((m) => !m)}
      onStop={endVoice}
      onCollapse={() => setState("active")}
    />
    ```

    <Warning>
      **`onStop` and `onCollapse` are not interchangeable.** Collapsing returns to
      `active` with voice still running; stopping ends voice mode entirely. The
      two controls sit next to each other in the strip, so wiring them to the
      same handler is an easy mistake with very different consequences for the
      user.
    </Warning>

    `label` is caller-supplied so the host can localize it and reflect its own state ("Talking", "Listening", …). The kit owns only the action names.

    ## Accessibility

    * Every control is a `button` with an explicit accessible name.
    * The collapsed control's name changes with state — "Start voice mode" when `idle`, "Voice mode controls" when `active`. The glow is purely visual, so the name is the only signal a screen-reader user gets.
    * The mute controls carry their state in their name ("Mute microphone" / "Unmute microphone") rather than a checked state — the two together would contradict, announcing an unmuted mic as "Mute microphone, not selected".
    * Each mute control also shows its state visually: the glyph crosses out and its tint turns critical, so a muted channel is not announced-only.
    * Built-in action labels are English until the kit gains an i18n layer; `label` is already caller-supplied.
  </Tab>

  <Tab title="VoiceModeFab Props">
    ```tsx theme={null}
    <VoiceModeFab state="active" onPress={expand} />
    ```

    ## `VoiceModeFab` Props

    Discriminated on `state`. It does not forward remaining React Native props.

    ### Collapsed — `state: "idle" | "active"`

    <ParamField path="state" type={`"idle" | "active"`} required>
      `idle` = voice available, not running. `active` = listening, collapsed
      (renders the presence glow).
    </ParamField>

    <ParamField path="onPress" type="() => void" required>
      Starts voice mode (from `idle`) or expands the control strip (from
      `active`).
    </ParamField>

    ### Expanded — `state: "expanded"`

    <ParamField path="state" type={`"expanded"`} required>
      Renders the control strip instead of the circle.
    </ParamField>

    <ParamField path="label" type="string" required>
      Status text, e.g. "Talking". Caller-supplied so the host can localize it.
    </ParamField>

    <ParamField path="isMicMuted" type="boolean" required>
      Whether the user's microphone is muted. Controlled.
    </ParamField>

    <ParamField path="onToggleMicMuted" type="() => void" required>
      Fired when the mic-mute control is pressed.
    </ParamField>

    <ParamField path="isSpeakerMuted" type="boolean">
      Whether Atlas's spoken output is muted. Controlled. Required together
      with `onToggleSpeakerMuted` — the two are typed as a pair, so the control
      can never render without the state that names it.
    </ParamField>

    <ParamField path="onToggleSpeakerMuted" type="() => void">
      Fired when the Atlas-audio control is pressed. Wiring it adds that control
      to the strip; without it the strip is mic, stop and collapse — the
      canonical design.
    </ParamField>

    <ParamField path="onStop" type="() => void" required>
      Ends voice mode entirely.
    </ParamField>

    <ParamField path="onCollapse" type="() => void" required>
      Collapses back to `active` **without** ending voice mode.
    </ParamField>

    ### Shared

    <ParamField path="analyticsId" type="string">
      Opt-in telemetry id. When set under `Anvil2RNTelemetryProvider`, each press
      emits a `press` event with `meta.action` (`start`, `expand`,
      `toggleMicMuted`, `toggleSpeakerMuted`, `stop`, `collapse`).
    </ParamField>

    <ParamField path="testID" type="string">
      Test hook forwarded to the root element.
    </ParamField>
  </Tab>
</Tabs>
