# Video meeting

Build video calls & devices with the room's shared state and capability-aware controls.

Import: @slices.sh/realtime/headless

Public parts: 48

## 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.Meeting
    Room.CallFrame
      Room.SidePanels panels={["transcript"]}
        Room.PanelArea
          Room.Stage
            Room.Tiles
              Room.Tile
                Room.TrackIs kind="screen"
                  Room.TileScreen
                Room.TrackIs kind="camera"
                  Room.TileVideo
                Room.TileAudio
                Room.CameraOff
                Room.Muted
                Room.Speaking
                Room.LinkQuality
                Room.Pin
                Room.Pinned
                Room.HandRaised
                Room.TileReaction
            Room.Overflow
          Room.SidePanel panel="transcript"
        Room.PanelToggle panel="transcript"
      Room.LocalVideo
      Room.LayoutMode mode="grid"
      Room.CurrentLayout
      Room.TileLimit
      Room.HideNoVideo
      Room.SelfView view="float"
      Room.RaiseHand
      Room.Hands
      Room.React emoji="❤️"
      Room.Flights
        Room.Flight
      Room.DeviceGroup kind="audioinput"
        Room.NoDevices
        Room.Devices
          Room.DeviceItem
      Room.DevicePicker kind="audioinput"
        Room.Devices
          Room.DeviceOption
      Room.ChosenDevice kind="audioinput"
      Room.MyLevel
      Room.MediaToggle kind="audio"
      Room.MediaDenied
      Room.MutedNudge
      Room.FullScreen
      Room.PictureInPicture
      Room.ShowShortcuts
      Room.ShortcutsSheet
```

## Minimal composition

```tsx
"use client"

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

export function Example() {
  return (
    <Room.Root room="studio">
      <Room.Meeting>
        <Room.Stage>
          <Room.Tiles>
            <Room.Tile>
              <Room.TileVideo />
              <Room.TileAudio />
              <Room.PeerName />
            </Room.Tile>
          </Room.Tiles>
        </Room.Stage>
        <Room.MediaToggle kind="audio">Microphone</Room.MediaToggle>
        <Room.MediaToggle kind="video">Camera</Room.MediaToggle>
        <Room.MediaDenied>
          Allow access to your camera and microphone.
        </Room.MediaDenied>
      </Room.Meeting>
    </Room.Root>
  )
}

```

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

## Complete API reference

### Room.Meeting

scope

A call's way of looking at the room: the layout this person chose, the tiles they pinned,
and who has the floor. Local to this browser — nobody's layout moves anybody else's.

Around the stage and the controls both, so a layout menu in a top bar and the stage it
changes read the one set of choices.

Import: @slices.sh/realtime/headless

```tsx
MeetingProps

type MeetingProps = RootProps &
  Partial<Pick<LayoutChoices, "mode" | "maxTiles" | "selfView" | "hideNoVideo">> & {
    /** How many tiles may be pinned at once; the oldest pin gives way past it. */
    pinLimit?: number
  }
```

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

### Room.CallFrame

content · Element: div

Meeting is this browser's view of a call: its layout, its pins, who has the floor. CallFrame is what full screen fills and where Meet's shortcuts listen. Stage measures the box, Tiles repeats tiles and Tile is one, with TrackIs kind="camera" | "screen" | "board". Pin and Pinned pin a tile. Overflow is the +N tile. LayoutMode, CurrentLayout, TileLimit, HideNoVideo and SelfView set the layout. FullScreen and PictureInPicture take the call elsewhere; ShortcutsSheet and ShowShortcuts list the keys.

Import: @slices.sh/realtime/headless

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

```tsx
CallFrameProps

type CallFrameProps = PartProps<"div", { fullScreen: boolean }> & {
  /** Hold Space to talk, anywhere in the call but a field or a button — a voice room's key. */
  spaceToTalk?: boolean
}
```

Source: @slices.sh/realtime/src/headless/call-frame.tsx

### Room.SidePanels

scope

Panels beside a call — one open at a time, chosen by each person for themselves. Inside a
`Room.CallFrame`, ⌃⌘C opens the chat, ⌃⌘P the people, ⌃⌘B the whiteboard and ⌃⌘T the
transcript, where the call has them and draws their `Room.SidePanel`.

Import: @slices.sh/realtime/headless

```tsx
SidePanelsProps

type SidePanelsProps = RootProps & {
  /** The panels this call has: what its toggles and shortcuts may open. */
  panels: readonly string[]
  initial?: string | null
}
```

Source: @slices.sh/realtime/src/headless/side-panels.tsx

### Room.PanelArea

content · Element: div

What holds the call and its panel: `data-state` names the open panel, for the layout to follow.

Import: @slices.sh/realtime/headless

Room.PanelArea must be inside <Room.SidePanels>.

```tsx
PartProps<"div", { open: string | null }>
```

Source: @slices.sh/realtime/src/headless/panel-area.tsx

### Room.Stage

content · Element: div

The call's stage: one measured box every tile is laid out in.

Tiles are one flat list of tracks positioned inside it, with rects from a pure layout
function — so a camera moving from the grid to the strip, or a screen taking the stage, is
the same element gliding, and a playing `<video>` is never remounted. `data-shape` says
what the layout came out as (`solo`, `one-on-one`, `grid`, `focus`, `spotlight`). Whoever
the room's follow is on takes the stage like a pinned tile, ahead of the pins.
→ hooks/realtime/track-layout.ts

Import: @slices.sh/realtime/headless

Room.Stage must be inside <Room.Meeting>.

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

```tsx
StageProps

type StageProps = PartProps<"div", { shape: string }> & {
  /** Room kept free inside the edges, in pixels, so a tile's ring is never clipped. */
  inset?: number
  /** Room kept free at the bottom, for captions or a control bar laid over the stage. */
  insetBottom?: number
}
```

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

### Room.Tiles

scope

Everything on the stage, one scope per track: a camera per person, and a screen for each
person sharing one. Keyed by track and never regrouped, so no tile changes parent when the
layout does.

Import: @slices.sh/realtime/headless

Room.Tiles must be inside <Room.Stage>.

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

```tsx
TilesProps

type TilesProps = RootProps & {
  /** What the tiles are grouped in: a Motion presence group lets somebody leave, not vanish. */
  group?: ComponentType<{ children?: ReactNode }>
}
```

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

### Room.Tile

content · Element: div

One track on the stage — a face, a shared screen — at the rect the layout gave it.

Absolutely placed inside `Room.Stage` and moved on the shared layout spring whenever the
layout changes, so a tile glides from the grid to the strip and its `<video>` keeps
playing: a tile never changes parent. `data-kind` is what it shows, `data-slot` where it
sits (`grid`, `stage`, `strip`, `float`, `hidden`), `data-pinned` whether you pinned it and
`data-followed` whether its person is the one you follow, for a skin to outline.
A hidden tile is inert and out of the accessibility tree; it still exists, so a camera
coming back into view does not have to start again.

`data-active` says the person is reachable — a connection is open — rather than only listed.

A camera tile carries its person's voice as `--level`, for a ring drawn in CSS. Enter or a
double click pins it. The floating self-view can be dragged to any corner.

Import: @slices.sh/realtime/headless

Room.Tile must be inside <Room.Tiles>.

Room.Tile must be inside <Room.Stage>.

Room.Tile must be inside <Room.Meeting>.

```tsx
PartProps<"div", TileState>

type TileState = { kind: string; slot: string; pinned: boolean; followed: boolean; self: boolean }
```

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

### Room.TrackIs

gate

The tile in scope shows this kind of track: a camera, a screen, the board.

Import: @slices.sh/realtime/headless

Room.TrackIs must be inside <Room.Tiles>.

```tsx
GateProps & { kind: TrackKind }
```

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

### Room.TileScreen

content · Element: video

What one peer is sharing.

Its own part rather than a second use of `Room.TileVideo`, because a camera and a screen
are two pictures and an element plays the first video track it is handed: one element
for both is one of them showing and the other silently lost. Rendered only while that
peer says it is sharing. → docs/rules/capability-honesty.md

Silent, like every tile: a shared screen's sound, where a browser gives any, is that
peer's audio and belongs to `Room.TileAudio`.

Import: @slices.sh/realtime/headless

Room.TileScreen must be inside <Room.Peers>.

```tsx
PartProps<"video">
```

Source: @slices.sh/realtime/src/headless/tile-screen.tsx

### Room.TileVideo

content · Element: video

One track's picture: a peer's camera on a camera tile, the screen they share on a screen
tile. Outside a stage, where a tile is a person, it is their camera.

Rendered only while that peer says the track is on, because a `<video>` with no track is a
black rectangle and a grid of them looks like an outage. What the peer *says* decides, not
whether a track happens to exist. → docs/rules/capability-honesty.md

Silent, always: the voice is `Room.TileAudio`'s job, so an unmuted tile would play it twice,
and your own unmuted tile is your microphone in your own speakers.

`data-state="mine"` on your own camera, which a skin flips: a self-view is a mirror. A
screen is never flipped, yours included — a mirrored slide is a slide nobody can read.

Import: @slices.sh/realtime/headless

Room.TileVideo must be inside <Room.Peers> or <Room.Tiles>.

```tsx
PartProps<"video">
```

Source: @slices.sh/realtime/src/headless/tile-video.tsx

### Room.TileAudio

content · Element: audio

One peer's sound.

Never your own: playing your own microphone back is a howl, and it is the first thing
anybody building a room gets wrong.

Import: @slices.sh/realtime/headless

Room.TileAudio must be inside <Room.Peers>.

```tsx
PartProps<"audio">
```

Source: @slices.sh/realtime/src/headless/tile-audio.tsx

### Room.CameraOff

gate

This peer has no picture on screen: where the initials go instead of one.

A camera somebody said was off and a camera nobody has heard about are the same thing to
look at — there is no picture either way — so `null` belongs here with `false`. It is the
badges that must not guess. → docs/rules/capability-honesty.md

Import: @slices.sh/realtime/headless

Room.CameraOff must be inside <Room.Peers>.

```tsx
GateProps
```

Source: @slices.sh/realtime/src/headless/camera-off.tsx

### Room.Muted

gate

This peer says its microphone is off.

What it says, not what its audio sounds like: silence is not mute, and a tile that
guessed would label every pause. → docs/rules/capability-honesty.md

And said, rather than merely not said: `micOn` is `null` for the second before a peer's
first control message and for as long as nobody can reach them, and a badge there was a
claim nobody had made — every new face arrived wearing a crossed-out microphone.

Import: @slices.sh/realtime/headless

Room.Muted must be inside <Room.Peers>.

```tsx
GateProps
```

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

### Room.Speaking

gate

This peer is making a noise a person would call talking.

Measured, not announced: a peer cannot tell you it is speaking any faster than it can send
you the sound. From the level the page already reads for rings, on one shared analyser, or
from the transport where it knows. → ./use-talking.ts

Import: @slices.sh/realtime/headless

Room.Speaking must be inside <Room.Peers>.

```tsx
GateProps
```

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

### Room.LinkQuality

content · Element: span

A badge for somebody whose connection is struggling: the answer to "did they freeze, or
did I?". Nothing while their link is fine, and nothing before it has been measured or where
it cannot be — a badge that always shows teaches everybody to stop reading it.
`data-state` is `fair` or `poor`; its children get the same, for words that say which.

Import: @slices.sh/realtime/headless

Room.LinkQuality must be inside <Room.Peers>.

```tsx
PartProps<"span", { quality: Quality }>
```

Source: @slices.sh/realtime/src/headless/link-quality.tsx

### Room.Pin

action · Element: button

Pin this tile for yourself, or unpin it. Pinned tiles take the stage, oldest first, and
nobody else's view moves. `aria-pressed` and `data-active` while pinned.

Import: @slices.sh/realtime/headless

Room.Pin must be inside <Room.Tiles>.

Room.Pin must be inside <Room.Meeting>.

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

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

### Room.Pinned

gate

You pinned the tile in scope.

Import: @slices.sh/realtime/headless

Room.Pinned must be inside <Room.Tiles>.

Room.Pinned must be inside <Room.Meeting>.

```tsx
GateProps
```

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

### Room.HandRaised

gate

This peer has a hand up, and where they are in the queue of raised hands, from 1.

Import: @slices.sh/realtime/headless

Room.HandRaised must be inside <Room.Peers> or <Room.Tiles>.

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

```tsx
GateProps<{ place: number }> & { group?: GateGroupComponent }
```

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

### Room.TileReaction

gate

The emoji this peer sent last, while it is still in the air: a badge on their tile. Each
emoji is an arrival of its own, so a second one replaces the first rather than relabelling it.

Import: @slices.sh/realtime/headless

Room.TileReaction must be inside <Room.Tiles>.

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

```tsx
GateProps<{ emoji: string; id: string }> & { group?: GateGroupComponent }
```

Source: @slices.sh/realtime/src/headless/tile-reaction.tsx

### Room.Overflow

content · Element: div

The last cell of a full stage: how many are not shown, and who. Nothing while everybody
fits. Placed and moved like a tile, on the same spring.

Drawn even for a mesh, which stops long before it needs one, so a transport that carries
more people later needs no new screen.

Import: @slices.sh/realtime/headless

Room.Overflow must be inside <Room.Stage>.

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

```tsx
PartProps<"div", OverflowState>

type OverflowState = { hidden: number; names: string[] }
```

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

### Room.SidePanel

content · Element: aside

One panel beside the call, drawn only while it is the open one. Escape closes it, from
anywhere inside it — the way out of a panel a person opened by accident. While mounted it is
what makes its panel reachable: inside a gate that hides it, its toggle and shortcut do nothing.

Import: @slices.sh/realtime/headless

Room.SidePanel must be inside <Room.SidePanels>.

```tsx
PartProps<"aside"> & { panel: string }
```

Source: @slices.sh/realtime/src/headless/side-panel.tsx

### Room.PanelToggle

action · Element: button

Opens one panel beside the call, or closes it. `aria-expanded` and `data-active` while open.

Import: @slices.sh/realtime/headless

Room.PanelToggle must be inside <Room.SidePanels>.

```tsx
PartProps<"button", { open: boolean }> & { panel: string }
```

Source: @slices.sh/realtime/src/headless/panel-toggle.tsx

### Room.LocalVideo

content · Element: video

Your own picture.

Muted, always: an unmuted local tile is your microphone coming out of your own speakers.

Import: @slices.sh/realtime/headless

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

```tsx
PartProps<"video">
```

Source: @slices.sh/realtime/src/headless/local-video.tsx

### Room.LayoutMode

action · Element: button

Choose how the stage is laid out. `aria-pressed` and `data-active` on the current one.

Import: @slices.sh/realtime/headless

Room.LayoutMode must be inside <Room.Meeting>.

```tsx
PartProps<"button", { active: boolean }> & { mode: Mode }
```

Source: @slices.sh/realtime/src/headless/layout-mode.tsx

### Room.CurrentLayout

content · Element: span

The layout in use, for a menu's trigger to name. `data-state` is the mode.

Import: @slices.sh/realtime/headless

Room.CurrentLayout must be inside <Room.Meeting>.

```tsx
PartProps<"span", { mode: LayoutMode }>
```

Source: @slices.sh/realtime/src/headless/current-layout.tsx

### Room.TileLimit

input · Element: input

How many tiles the stage shows before the rest are counted on a "+N" tile.

Import: @slices.sh/realtime/headless

Room.TileLimit must be inside <Room.Meeting>.

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

Source: @slices.sh/realtime/src/headless/tile-limit.tsx

### Room.HideNoVideo

action · Element: button

Leave out the tiles of people whose camera is off, unless they are pinned. A switch.

Import: @slices.sh/realtime/headless

Room.HideNoVideo must be inside <Room.Meeting>.

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

Source: @slices.sh/realtime/src/headless/hide-no-video.tsx

### Room.SelfView

action · Element: button

Where your own picture goes: chosen for you, in the grid, floating, or nowhere.

Import: @slices.sh/realtime/headless

Room.SelfView must be inside <Room.Meeting>.

```tsx
PartProps<"button", { active: boolean }> & { view: View }
```

Source: @slices.sh/realtime/src/headless/self-view.tsx

### Room.RaiseHand

action · Element: button

Put your hand up, or down. Everybody sees it, in the order hands went up. `aria-pressed`
and `data-active` while it is up.

Import: @slices.sh/realtime/headless

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

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

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

### Room.Hands

scope

The queue of raised hands, first raised first, each in its peer's scope.

Import: @slices.sh/realtime/headless

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

```tsx
RootProps
```

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

### Room.React

action · Element: button

Send an emoji to the room: it rises from the edge for everybody, with your name.

Import: @slices.sh/realtime/headless

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

```tsx
PartProps<"button"> & { emoji: string }
```

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

### Room.Flights

scope

Emoji in the air, each in its own scope with its lane and its sender.

A sender keeps one lane while their emoji are flying, so two people's names never land on
each other, and only the first emoji of a burst carries the name. → hooks/realtime/use-reactions.ts

Import: @slices.sh/realtime/headless

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

```tsx
FlightsProps

type FlightsProps = RootProps & {
  /** What the flights are grouped in: a Motion presence group lets each one finish rising. */
  group?: ComponentType<{ children?: ReactNode }>
}
```

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

### Room.Flight

content · Element: div

One emoji rising. `--lane` is its lane from 0, for a skin to place it; `named` says whether
it is the first of its sender's burst, which is the one that carries the name.

Import: @slices.sh/realtime/headless

Room.Flight must be inside <Room.Flights>.

```tsx
PartProps<"div", FlightRecord>
```

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

### Room.DeviceGroup

content · Element: div

The devices of one kind as a choice of one, for a menu's radio group: its `value` is the
device in use and choosing another switches to it, or remembers it for when the camera or
microphone is next turned on. Items are `Room.Devices` with `Room.DeviceItem` inside.

Import: @slices.sh/realtime/headless

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

```tsx
DeviceGroupProps

type DeviceGroupProps = PartProps<"div", { chosen: string | null }> & { kind: "audioinput" | "videoinput" }
```

Source: @slices.sh/realtime/src/headless/device-group.tsx

### Room.NoDevices

gate

The browser admits to no device of this kind yet — usually because nobody has been asked
for permission, which is when a list is empty rather than when there is nothing plugged in.

Import: @slices.sh/realtime/headless

Room.NoDevices must be inside <Room.DeviceGroup>.

```tsx
GateProps
```

Source: @slices.sh/realtime/src/headless/no-devices.tsx

### Room.Devices

scope

Every device the picker around this is offering, each in its own scope.

Import: @slices.sh/realtime/headless

Room.Devices must be inside <Room.DevicePicker>.

```tsx
RootProps
```

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

### Room.DeviceItem

content · Element: div

One device in a `Room.DeviceGroup`, named as the browser names it.

Import: @slices.sh/realtime/headless

Room.DeviceItem must be inside <Room.Devices>.

```tsx
PartProps<"div">
```

Source: @slices.sh/realtime/src/headless/device-item.tsx

### Room.DevicePicker

input · Element: select

Which microphone or camera to use.

Choosing one while it is off remembers the choice, and the device is opened when the
toggle beside it is turned on — so picking from a list never switches a light on by
itself. The remembering is `useLocalMedia`'s, because that is what owns `set`.

A list of one is not a choice, and a list of none is a browser that has not been asked
for permission yet — neither is worth a control, so neither draws one.

Import: @slices.sh/realtime/headless

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

```tsx
DevicePickerProps

type DevicePickerProps = PartProps<"select"> & { kind: "audioinput" | "videoinput" }
```

Source: @slices.sh/realtime/src/headless/device-picker.tsx

### Room.DeviceOption

content · Element: option

One device in the list.

The label is empty until permission has been given once — that is the browser keeping the
list from being a fingerprint, not a bug — so before permission this reads "Microphone 1"
and after it reads the real name. → hooks/realtime/use-devices.ts

Import: @slices.sh/realtime/headless

Room.DeviceOption must be inside <Room.Devices>.

```tsx
PartProps<"option">
```

Source: @slices.sh/realtime/src/headless/device-option.tsx

### Room.ChosenDevice

content · Element: span

The name of the camera or microphone in use, for a menu's trigger: what a person checks
before a call, and cannot see on a button that only says "Microphone". Its children stand
in until the browser names a device, which it does only once one was allowed.

Import: @slices.sh/realtime/headless

Room.ChosenDevice must be inside <Room.Root> or <Join.Root>.

```tsx
PartProps<"span"> & { kind: "audioinput" | "videoinput" }
```

Source: @slices.sh/realtime/src/headless/chosen-device.tsx

### Room.MyLevel

content · Element: span

Your own microphone's level as `--level`, for a meter beside the microphone button: the one
way to know a microphone is working before anybody has to ask whether you can be heard.

Import: @slices.sh/realtime/headless

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

```tsx
PartProps<"span">
```

Source: @slices.sh/realtime/src/headless/my-level.tsx

### Room.MediaToggle

action · Element: button

Turns one kind of track on or off. `data-active` while it is on.

Disabled while there is no room to send into, because there is nothing for a camera to
reach and the light would come on anyway: a control bar that still worked over a room
that was full or refused was the loudest thing wrong with a meeting that had failed.
→ docs/rules/capability-honesty.md

Import: @slices.sh/realtime/headless

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

```tsx
MediaToggleProps

type MediaToggleProps = PartProps<"button", { on: boolean }> & { kind: MediaKind }
```

Source: @slices.sh/realtime/src/headless/media-toggle.tsx

### Room.MediaDenied

gate

The browser refused the camera or the microphone.

`unsupported` is told apart from `denied` because the remedies are different and only one
of them exists: a permission can be changed in the address bar, and a browser without
WebRTC cannot be talked round. → docs/rules/capability-honesty.md

Import: @slices.sh/realtime/headless

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

```tsx
GateProps<{ unsupported: boolean }>
```

Source: @slices.sh/realtime/src/headless/media-denied.tsx

### Room.MutedNudge

gate

You are talking and nobody can hear you: where a call says so, with a way to unmute.
Rendering it is what starts the listening — on this device only, and only where the
microphone was already allowed. → hooks/realtime/use-muted-nudge.ts

Import: @slices.sh/realtime/headless

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

```tsx
GateProps & { group?: GateGroupComponent }
```

Source: @slices.sh/realtime/src/headless/muted-nudge.tsx

### Room.FullScreen

action · Element: button

Fill the screen with the call, or leave full screen. Nothing where the browser cannot —
an iPhone's Safari, a frame without permission — rather than a button that does nothing.

Import: @slices.sh/realtime/headless

Room.FullScreen must be inside <Room.CallFrame>.

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

Source: @slices.sh/realtime/src/headless/full-screen.tsx

### Room.PictureInPicture

action · Element: button

Floats the video worth watching — a shared screen, then whoever is speaking — over other
windows, and brings it back. `aria-pressed` while it floats; nothing in a browser that cannot.

Import: @slices.sh/realtime/headless

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

Source: @slices.sh/realtime/src/headless/picture-in-picture.tsx

### Room.ShowShortcuts

action · Element: button

Open the sheet of keyboard shortcuts, for somebody who did not know to press `?`.

Import: @slices.sh/realtime/headless

Room.ShowShortcuts must be inside <Room.CallFrame>.

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

Source: @slices.sh/realtime/src/headless/show-shortcuts.tsx

### Room.ShortcutsSheet

content

The sheet that lists the call's keys, as a dialog root: `open` and `onOpenChange` are
handed to whatever it renders, so a skin's dialog opens on `?` and closes on Escape.

Import: @slices.sh/realtime/headless

Room.ShortcutsSheet must be inside <Room.CallFrame>.

```tsx
PartProps<null>
```

Source: @slices.sh/realtime/src/headless/shortcuts-sheet.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
