@retor/react
v0.4.6
Published
React SDK for embedding Retor 3D experiences
Readme
@retor/react
Embed Retor 3D experiences in a React app — with composable bottom-sheet UI components that mirror the Retor preview.
Installation
npm install @retor/react lucide-react
# or
pnpm add @retor/react lucide-react
# or
yarn add @retor/react lucide-react
lucide-reactis a peer dependency — used by the default tag list items to render icon-type tags.
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>
);
}What you get:
- A bottom card showing project name + horizontal carousel of lines
- Tap a line → detail sheet with tags, autoplay button, and Done
- Tap a tag → camera scrolls to it
- Done returns to browse view
Concepts
<Viewer>wraps the iframe and exposes scene state via React context. Always vanilla — Retor itself shows no UI.<Hud>is a positioned overlay container. Place sheets and other overlays inside.- Sheets auto-show / hide based on bridge state:
<ProjectSheet>— when no line is open<LineDetailSheet>— when a line is open<AddNoteSheet>— whenuseAddNote().open()is called
- Composition components (
<LinesCarousel>,<LineTagList>) take a render prop so you can replace the visuals while keeping the data wiring.
Customising the visuals
Each sheet accepts:
renderHeader— replace the headerchildren— replace the body (typically a render-prop list)
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
<AddNoteSheet> triggers when you call useAddNote().open(tagId?). It collects text + private/public, then either:
- calls
onNoteSubmiton the parent<Viewer>(if set) — persistence is your responsibility - or falls back to persisting via Retor's own backend (Convex) when no
onNoteSubmitis provided
Re-pass saved notes back to the 3D scene via <Notes>.
Note fields for proper rendering
When passing notes via <Notes>, each note should include:
| Field | Required | Purpose |
|-------|----------|---------|
| _id | yes | Unique identifier |
| name | yes | The note text (displayed in the tag list and 3D scene) |
| position | yes | { x, y, z } — where the note sits on the line |
| objectId | yes | Set to the lineId so the note associates with the correct line |
| progress | yes | 0..1 position along the line (from the submit payload) — used for sort order and scroll-to |
| avatarUrl | recommended | Profile image URL — renders as a circular avatar in the tag pill and list item |
| authorName | recommended | Display name — used as initial-letter fallback when no avatarUrl |
| tagType | recommended | Set to "icon" for the standard note pill appearance |
| userId | for deletion | The note author's user ID — compared against Viewer.userId to show the delete button |
Creating + deleting notes
import { useState } from "react";
import { Viewer, Hud, ProjectSheet, LineDetailSheet, AddNoteSheet, Notes, type RetorTag } from "@retor/react";
export default function Scene() {
const [notes, setNotes] = useState<RetorTag[]>([]);
const currentUserId = "user_abc"; // your auth system's user ID
return (
<Viewer
projectId="abc123"
userId={currentUserId}
onNoteSubmit={({ text, lineId, position, progress }) => {
if (!position) return;
setNotes((prev) => [
...prev,
{
_id: `note-${Date.now()}`,
name: text,
position,
progress,
objectId: lineId ?? undefined,
tagType: "icon",
avatarUrl: "https://example.com/avatar.jpg",
authorName: "Jane",
userId: currentUserId,
},
]);
}}
onNoteDelete={(noteId) => {
setNotes((prev) => prev.filter((n) => n._id !== noteId));
// also delete from your backend
}}
>
<Notes notes={notes} />
<Hud>
<ProjectSheet />
<LineDetailSheet />
<AddNoteSheet />
</Hud>
</Viewer>
);
}When onNoteSubmit is not provided, the SDK sends the note to Retor's backend automatically (using the signed-in Clerk session inside the iframe). No <Notes> re-injection needed in that case.
When a user deletes a note, the SDK:
- Optimistically removes it from the local tag list
- Fires
onNoteDelete(noteId)so you can delete from your backend
Hooks
All hooks read from the bridge context provided by the parent <Viewer>.
| Hook | Returns |
|------|---------|
| useProject() | The current RetorProject (or null) |
| useLines() | Array of RetorLine |
| useActiveLine() | The currently open line (or null) |
| useLineProgress() | { progress, closestTagId } |
| useAutoplay() | { isPlaying, toggle, play, pause } |
| useAddNote() | { isOpen, tagId, open, close, submit } |
| useViewer() | Imperative controls (openLine, exitLine, scrollToTag, scrollToProgress, ...) |
Imperative API
The useViewer hook also supports controlling a specific viewer by ID:
<Viewer id="left" projectId="..." />
<Viewer id="right" projectId="..." />
const left = useViewer("left");
left.openLine("line-a");Or pass a ref directly:
const ref = useRef<ViewerHandle>(null);
<Viewer ref={ref} projectId="..." />
ref.current?.openLine("...");Cover photo
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 }} />License
MIT
