# Voice room

Build voice stage with the room's shared state and capability-aware controls.

Import: @slices.sh/realtime/headless

Public parts: 14

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/realtime@^0.1.6
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/realtime.md

## Anatomy

### Composition

```text
Room.Root room="standup"
  Room.SeatCount
  Room.Seats side="stage"
    Room.Seat
      Room.IsHost
      Room.SeatReactions
      Room.Volume
      Room.Hosting
        Room.MoveToAudience
        Room.Invite
        Room.LowerHand
  Room.Asking
    Room.Requests
  Room.PushToTalk
  Room.Deafen
```

## Minimal composition

```tsx
"use client"

import "@/services"
import { Room } from "@slices.sh/realtime/headless"

export function Example() {
  return (
    <Room.Root room="studio">
      <Room.Seats side="stage">
        <Room.Seat>
          <Room.PeerAvatar />
          <Room.PeerName />
          <Room.SeatReactions />
        </Room.Seat>
      </Room.Seats>
      <Room.RaiseHand>Ask to speak</Room.RaiseHand>
      <Room.LowerHand>Lower hand</Room.LowerHand>
      <Room.PushToTalk>Hold to talk</Room.PushToTalk>
    </Room.Root>
  )
}

```

Configure realtime. Media, captions and moderation controls hide when the provider does not grant their capability.

## Complete API reference

### Room.SeatCount

content · Element: span

How many are on one side of the room.

Import: @slices.sh/realtime/headless

Room.SeatCount must be inside <Room.Root>.

```tsx
PartProps<"span", { count: number }> & { side: Side }
```

Source: @slices.sh/realtime/src/headless/seat-count.tsx

### Room.Seats

scope

The people on one side of a voice room, each in their own scope: the stage, or the audience.

Keyed by person on both sides, so somebody invited up is the same seat moving across rather
than one seat leaving and another arriving — which a shared layout animation draws as a
face travelling to the stage.

Import: @slices.sh/realtime/headless

Room.Seats must be inside <Room.Root>.

```tsx
SeatsProps

type SeatsProps = RootProps & {
  side: Side
  /** Everybody on this side, whatever they claim: a room with no stage is one circle. */
  everybody?: boolean
  /** What the seats are grouped in: a Motion presence group lets a seat arrive and leave. */
  group?: ComponentType<{ children?: ReactNode }>
}
```

Source: @slices.sh/realtime/src/headless/seats.tsx

### Room.Seat

content · Element: li

One person's seat in a voice room. `data-state` is `stage` or `audience`, and the seat
carries their voice as `--level`, for a ring that grows as they speak.

Import: @slices.sh/realtime/headless

Room.Seat must be inside <Room.Seats>.

Room.Seat must be inside <Room.Root>.

```tsx
PartProps<"li", { side: Side }>
```

Source: @slices.sh/realtime/src/headless/seat.tsx

### Room.IsHost

gate

The person in scope runs the room.

Import: @slices.sh/realtime/headless

Room.IsHost must be inside <Room.Seats>.

Room.IsHost must be inside <Room.Root>.

```tsx
GateProps
```

Source: @slices.sh/realtime/src/headless/is-host.tsx

### Room.SeatReactions

scope

The emoji the person in scope sent, while they float up from their seat.

Import: @slices.sh/realtime/headless

Room.SeatReactions must be inside <Room.Seats>.

Room.SeatReactions must be inside <Room.Root>.

```tsx
SeatReactionsProps

type SeatReactionsProps = RootProps & { group?: ComponentType<{ children?: ReactNode }> }
```

Source: @slices.sh/realtime/src/headless/seat-reactions.tsx

### Room.Volume

input · Element: input

How loud the person in scope is for you alone, from 0 to 100. Nobody else is told.

Import: @slices.sh/realtime/headless

Room.Volume must be inside <Room.Seats>.

Room.Volume must be inside <Room.Root>.

```tsx
PartProps<"input", { volume: number }>
```

Source: @slices.sh/realtime/src/headless/volume.tsx

### Room.Hosting

gate

You run the room: what only a host may do — invite, move, dismiss — is drawn here.

Import: @slices.sh/realtime/headless

Room.Hosting must be inside <Room.Root>.

```tsx
GateProps
```

Source: @slices.sh/realtime/src/headless/hosting.tsx

### Room.MoveToAudience

action · Element: button

Ask the speaker in scope to step down into the audience. Only a host's button does anything, so only a host's is enabled.

Import: @slices.sh/realtime/headless

Room.MoveToAudience must be inside <Room.Seats>.

Room.MoveToAudience must be inside <Room.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/realtime/src/headless/move-to-audience.tsx

### Room.Invite

action · Element: button

Ask the person in scope up onto the stage. They move when their own browser says so. Only a host's button does anything, so only a host's is enabled.

Import: @slices.sh/realtime/headless

Room.Invite must be inside <Room.Seats>.

Room.Invite must be inside <Room.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/realtime/src/headless/invite.tsx

### Room.LowerHand

action · Element: button

Put somebody's hand down for them, as a host answering their request. Only a host's button does anything, so only a host's is enabled.

Import: @slices.sh/realtime/headless

Room.LowerHand must be inside <Room.Seats>.

Room.LowerHand must be inside <Room.Root>.

```tsx
PartProps<"button">
```

Source: @slices.sh/realtime/src/headless/lower-hand.tsx

### Room.Asking

gate

Somebody in the audience is asking to speak, and how many.

Import: @slices.sh/realtime/headless

Room.Asking must be inside <Room.Root>.

```tsx
GateProps<{ count: number }>
```

Source: @slices.sh/realtime/src/headless/asking.tsx

### Room.Requests

scope

The audience's raised hands, first raised first: who is asking to speak, and in what order.

Import: @slices.sh/realtime/headless

Room.Requests must be inside <Room.Root>.

```tsx
RootProps
```

Source: @slices.sh/realtime/src/headless/requests.tsx

### Room.PushToTalk

action · Element: button

Hold to talk: the microphone is on while this is held, and off the moment it is let go —
or the pointer leaves it, or the window loses focus, because a talk button stuck down is a
room that hears everything. Nobody in the audience can hold it; they raise a hand instead.
`data-active` while talking.

Import: @slices.sh/realtime/headless

Room.PushToTalk must be inside <Room.Root>.

```tsx
PartProps<"button", { talking: boolean }>
```

Source: @slices.sh/realtime/src/headless/push-to-talk.tsx

### Room.Deafen

action · Element: button

Hear nobody, or everybody again. Local: nobody is told you stopped listening.

Import: @slices.sh/realtime/headless

Room.Deafen must be inside <Room.Root>.

```tsx
PartProps<"button", { deafened: boolean }>
```

Source: @slices.sh/realtime/src/headless/deafen.tsx

### Room.Root

scope

The room everything under here is in.

Joining happens in the provider's effect rather than here, so a component that renders
twice before it commits does not put two peers in the roster for one tab.

A root inside a root for the same room shares its connection, and its follow too: a
presence list placed on a canvas has to move the canvas when somebody on it is picked, and
two follows would be two lists of whom somebody is following. A root inside a root for the
same room needs no service of its own, and says nothing when there is none.

Optional with no service, or off, there is no room: its status is "off", so every gate stays
shut and `Room.Present` hides what the room is drawn inside, and its sink is switched off, so
a Composer inside renders nothing. → ../hooks/use-realtime-scope.ts

Import: @slices.sh/realtime/headless

```tsx
RoomRootProps

type RoomRootProps = RootProps & RealtimeRootProps & {
  room: string
  /**
   * Who this browser shows itself as. Without one, a visitor gets an animal of their own; and
   * somebody signed in is their session's name and picture whatever this says.
   */
  profile?: PeerProfile
  /**
   * One person across this browser's tabs: every tab takes the same name, the one a screen
   * that counts people shows once. Off, each tab is somebody of its own.
   */
  perPerson?: boolean
  /** Off, the transport never asks for a camera or a microphone. */
  media?: boolean
  /** Captions start shown; each person can still hide them for themselves. */
  captions?: boolean
}
```

Source: @slices.sh/realtime/src/headless/room-root.tsx
