npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-react is 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> — when useAddNote().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 header
  • children — 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 onNoteSubmit on the parent <Viewer> (if set) — persistence is your responsibility
  • or falls back to persisting via Retor's own backend (Convex) when no onNoteSubmit is 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:

  1. Optimistically removes it from the local tag list
  2. 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