# Annotate together

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

Import: @slices.sh/realtime/headless

Public parts: 10

## 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.Drawing
    Room.DrawingSurface
      Room.Strokes
      Room.DrawingCaret
    Room.DrawTool tool="pen"
    Room.DrawColor color="#4779ee"
    Room.DrawWidth width={3}
    Room.UndoStroke
    Room.ClearMine
    Room.ClearDrawing
```

## Minimal composition

```tsx
"use client"

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

export function Example() {
  return (
    <Room.Root room="studio">
      <Room.Drawing>
        <Room.DrawingSurface>
          <Room.Strokes />
        </Room.DrawingSurface>
        <Room.DrawTool tool="pen">Pen</Room.DrawTool>
        <Room.UndoStroke>Undo</Room.UndoStroke>
        <Room.ClearMine>Clear my strokes</Room.ClearMine>
      </Room.Drawing>
    </Room.Root>
  )
}

```

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

## Complete API reference

### Room.Drawing

scope

The shared drawing, the thing this person is drawing with, and the drawing they are doing.

Three concerns and three contexts: choosing a colour must not redraw the surface, and a
stroke arriving must not redraw the toolbar. → docs/rules/no-wide-context.md

Import: @slices.sh/realtime/headless

```tsx
RootProps
```

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

### Room.DrawingSurface

content · Element: svg

The surface, as an `<svg>`.

A viewBox of 0–1 in both directions, matching the fractions strokes travel in, so the
same drawing fits whatever size the element happens to be — and two people looking at
different window sizes see the same picture rather than the same pixels.

`touch-action: none` is what stops a phone scrolling the page instead of drawing on it.

`role="application"` and a tab stop, because this is a control rather than a picture: it
takes arrow keys, and a screen reader that passed them to its own reading cursor would
leave a keyboard user with no way to draw anything at all. The label is the screen's to
write. → hooks/realtime/use-drawing-input.ts

Import: @slices.sh/realtime/headless

Room.DrawingSurface must be inside <Room.Drawing>.

```tsx
PartProps<"svg", { tool: string }>
```

Source: @slices.sh/realtime/src/headless/drawing-surface.tsx

### Room.Strokes

scope

Visual renderer — not purely headless.

Every stroke on the surface, oldest underneath.

Drawn here rather than handed out one scope at a time, because a stroke is a `<path>` and
there is nothing about it a skin would change that `Room.DrawingSurface` cannot: the
colour and the width belong to whoever drew it, not to the theme.

`data-stroke` is how the eraser knows what it is over: a stroked path with no fill is hit
only along the line itself, so the browser's own hit testing is the hit testing.

Import: @slices.sh/realtime/headless

Room.Strokes must be inside <Room.Drawing>.

```tsx
RootProps
```

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

### Room.DrawingCaret

content · Element: circle

Where a keyboard is drawing from.

Nothing at all until somebody uses one, because a crosshair on a surface nobody is
navigating by keyboard is a mark with no meaning. Placed in the surface's own
coordinates, so it belongs inside the `<svg>`. → docs/rules/capability-honesty.md

Import: @slices.sh/realtime/headless

Room.DrawingCaret must be inside <Room.Drawing>.

```tsx
PartProps<"circle", { drawing: boolean }>
```

Source: @slices.sh/realtime/src/headless/drawing-caret.tsx

### Room.DrawTool

action · Element: button

Draw, or erase.

`data-state="selected"` rather than a conditional in the screen, so a toolbar is a row of
these and never a branch. → docs/rules/states-gate-themselves.md

Import: @slices.sh/realtime/headless

Room.DrawTool must be inside <Room.Drawing>.

```tsx
DrawToolProps

type DrawToolProps = PartProps<"button", { selected: boolean }> & { tool: DrawTool }
```

Source: @slices.sh/realtime/src/headless/draw-tool.tsx

### Room.DrawColor

action · Element: button

One colour to draw in. The swatch is its own colour, which is data rather than a look.

Import: @slices.sh/realtime/headless

Room.DrawColor must be inside <Room.Drawing>.

```tsx
DrawColorProps

type DrawColorProps = PartProps<"button", { selected: boolean }> & { value: string }
```

Source: @slices.sh/realtime/src/headless/draw-color.tsx

### Room.DrawWidth

action · Element: button

One line thickness.

Import: @slices.sh/realtime/headless

Room.DrawWidth must be inside <Room.Drawing>.

```tsx
DrawWidthProps

type DrawWidthProps = PartProps<"button", { selected: boolean }> & { value: number }
```

Source: @slices.sh/realtime/src/headless/draw-width.tsx

### Room.UndoStroke

action · Element: button

Take your own last stroke back.

Yours, and only yours: an undo that reached across a shared surface would be one person
able to unpick another's work, which is a different feature and a worse one. Disabled
when you have drawn nothing, because there is nothing to take back.

Import: @slices.sh/realtime/headless

Room.UndoStroke must be inside <Room.Drawing>.

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

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

Source: @slices.sh/realtime/src/headless/undo-stroke.tsx

### Room.ClearMine

action · Element: button

Take back everything you drew, and nothing anybody else did.

The safe half of clearing, and the one a person reaches for nine times out of ten: it
destroys only your own work, so it needs no confirmation and no undo.

Import: @slices.sh/realtime/headless

Room.ClearMine must be inside <Room.Drawing>.

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

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

Source: @slices.sh/realtime/src/headless/clear-mine.tsx

### Room.ClearDrawing

action · Element: button

Wipes the surface, for everybody.

There is no undo across a shared document, so this destroys work somebody else may have
spent an hour on and cannot get back. It is the one action in this family that a screen
must put behind a confirmation — `Room.ClearMine` is the one that needs none, and is what
most people actually want. Disabled while the surface is already empty.

Import: @slices.sh/realtime/headless

Room.ClearDrawing must be inside <Room.Drawing>.

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

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