React SDK
Embed Retor in a React app with composable bottom-sheet UI.
Installation
npm install @retor/reactQuick 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>.
| Hook | Returns |
|---|---|
| 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 }} />