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-native

v0.4.6

Published

React Native SDK for embedding Retor 3D experiences

Readme

@retor/react-native

Embed Retor 3D experiences in a React Native / Expo app — with composable bottom-sheet UI built on @gorhom/bottom-sheet.

Installation

# Expo
npx expo install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg expo-blur
npm install @gorhom/bottom-sheet lucide-react-native @retor/react-native

# bare React Native
npm install react-native-webview react-native-gesture-handler react-native-reanimated react-native-svg @gorhom/bottom-sheet lucide-react-native expo-blur @retor/react-native
cd ios && pod install

expo-blur is optional — it's used for the default blurred sheet background. Skip it if you don't want blur and pass a custom backgroundComponent to any sheet.

You also need to wrap your app root in a GestureHandlerRootView (per @gorhom/bottom-sheet requirements):

import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <YourApp />
    </GestureHandlerRootView>
  );
}

Quick Start — Default UI

The fastest way: drop in <Hud> with the three sheets and they'll work out of the box.

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

export default function Scene() {
  return (
    <View style={{ flex: 1 }}>
      <Viewer projectId="abc123">
        <Hud>
          <ProjectSheet />
          <LineDetailSheet />
          <AddNoteSheet />
        </Hud>
      </Viewer>
    </View>
  );
}

What you get:

  • A bottom sheet showing the project name and a horizontal carousel of lines
  • Tap a line → second sheet opens with the tag list and a Done button
  • Tap a tag → camera scrolls to it
  • Done returns to the browse sheet

Concepts

  • <Viewer> wraps the WebView and exposes scene state via React context. Always vanilla — Retor itself shows no UI.
  • <Hud> sets up BottomSheetModalProvider. Place sheets and other overlays inside.
  • Sheets auto-present based on bridge state:
    • <ProjectSheet> shows when no line is open
    • <LineDetailSheet> shows when a line is open
    • <AddNoteSheet> shows 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:

  • snapPoints — override the default snap points
  • renderHeader — replace the header
  • children — replace the body (typically a render-prop list)
import { Pressable, Text } from "react-native";
import { ProjectSheet, LinesCarousel, LineDetailSheet, LineTagList, useViewer } from "@retor/react-native";

function MyLineCard({ line }: { line: RetorLine }) {
  const { openLine } = useViewer();
  return (
    <Pressable
      onPress={() => openLine(line._id)}
      style={{ width: 200, padding: 16, backgroundColor: "#222", borderRadius: 16 }}
    >
      <Text style={{ color: "white", fontWeight: "600" }}>{line.name}</Text>
    </Pressable>
  );
}

<ProjectSheet snapPoints={["20%", "50%"]}>
  <LinesCarousel>
    {(line) => <MyLineCard line={line} />}
  </LinesCarousel>
</ProjectSheet>

<LineDetailSheet>
  <LineTagList>
    {(tag, isActive) => (
      <Pressable style={{ padding: 12 }}>
        <Text style={{ color: isActive ? "white" : "gray" }}>{tag.name}</Text>
      </Pressable>
    )}
  </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-native";

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, isPrivate, 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 WebView). 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-native";

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

License

MIT