React SDK

Embed Retor in a React app with composable bottom-sheet UI.

Installation

npm install @retor/react

Quick Start — Default UI

import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet } from "@retor/react";

export default function Scene() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Viewer projectId="abc123">
        <Hud>
          <ProjectSheet />
          <LineDetailSheet />
          <AddNoteSheet />
        </Hud>
      </Viewer>
    </div>
  );
}

Drop in the three sheets and they work out of the box — browse, open a line, scroll through tags, add notes. Visuals match the Retor preview.

Customising the visuals

Each sheet accepts renderHeader and a children slot for the body. Use the composable list components <LinesCarousel> and <LineTagList>with render-prop children to swap in your own item visuals while keeping the data wiring.

import { ProjectSheet, LinesCarousel, LineDetailSheet, LineTagList, useViewer, type RetorLine } from "@retor/react";

function MyLineCard({ line }: { line: RetorLine }) {
  const { openLine } = useViewer();
  return (
    <button
      onClick={() => openLine(line._id)}
      style={{ width: 200, padding: 16, background: "#222", borderRadius: 16, color: "white" }}
    >
      {line.name}
    </button>
  );
}

<ProjectSheet>
  <LinesCarousel>
    {(line) => <MyLineCard line={line} />}
  </LinesCarousel>
</ProjectSheet>

<LineDetailSheet>
  <LineTagList>
    {(tag, isActive) => (
      <button style={{ padding: 12, color: isActive ? "white" : "gray" }}>
        {tag.name}
      </button>
    )}
  </LineTagList>
</LineDetailSheet>

Notes

The note flow is triggered by calling useAddNote().open(tagId?) — typically from a "+" button on the active tag. <AddNoteSheet> presents, collects text + a private/public toggle, then fires onNoteSubmit on the parent <Viewer>.

Persistence is your responsibility. Store the note however you like and re-pass updated notes back via<Notes> — they'll merge into the line's tag list.

import { useState } from "react";
import {
  Viewer, Hud, ProjectSheet, LineDetailSheet, LineTagList, AddNoteSheet, Notes,
  useAddNote, type RetorTag,
} from "@retor/react";

function AddButton() {
  const { open } = useAddNote();
  return <button onClick={() => open()}>+</button>;
}

export default function Scene() {
  const [notes, setNotes] = useState<RetorTag[]>([]);

  return (
    <Viewer
      projectId="abc123"
      onNoteSubmit={({ text, tagId, lineId, position }) => {
        if (!position) return;
        setNotes((prev) => [
          ...prev,
          { _id: `note-${Date.now()}`, name: text, position, objectId: lineId ?? undefined },
        ]);
      }}
    >
      <Notes notes={notes} />
      <Hud>
        <ProjectSheet />
        <LineDetailSheet>
          <LineTagList>
            {(tag, isActive) => (
              <div style={{ display: "flex", padding: 12 }}>
                <span style={{ flex: 1, color: isActive ? "white" : "gray" }}>{tag.name}</span>
                {isActive && <AddButton />}
              </div>
            )}
          </LineTagList>
        </LineDetailSheet>
        <AddNoteSheet />
      </Hud>
    </Viewer>
  );
}

Hooks

All hooks read from the bridge context provided by the parent <Viewer>.

HookReturns
useProject()Project metadata (name, description)
useLines()Array of lines
useActiveLine()Currently open line (or null)
useLineProgress(){ progress, closestTagId }
useAutoplay(){ isPlaying, toggle, play, pause }
useAddNote(){ isOpen, tagId, open, close, submit }
useViewer()Imperative controls (openLine, exitLine, ...)

Imperative API

The useViewer hook also accepts an explicit ID or a ref:

// Multiple viewers
<Viewer id="left" projectId="..." />
<Viewer id="right" projectId="..." />
const left = useViewer("left");

// Or via ref
const ref = useRef<ViewerHandle>(null);
<Viewer ref={ref} projectId="..." />
ref.current?.openLine("...");

CoverPhoto

A static thumbnail of a project's start view — no 3D, no bridge:

import { CoverPhoto } from "@retor/react";

<CoverPhoto projectId="abc123" style={{ width: 200, height: 120 }} />