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

@remotedraw/react-native

v0.5.0

Published

RemoteDraw capture components and receiver for React Native and Expo.

Readme

@remotedraw/react-native

No component has run on a simulator or a device yet (see "What is tested" below).

Status: In progress on iOS. Tested in Node on macOS and Linux; not yet run in an iOS app. The API may still change. Desktop (Windows, macOS) is a spike (docs/desktop-spike.md). See the status legend.

Two layers:

  • Capture components (capture contract v1, docs/sdk/capture-contract.md): RemoteDrawCapture, RemoteDrawButton, PairingCode, SessionStatus, RemoteDrawSurface. The same names, inputs, states and events as the React, SwiftUI, Compose and Flutter kits, drawn from the generated design foundations (src/generated/design.ts) with react-native-svg.
  • Receiver primitives (0.1.0-dev.1, unchanged): createNativeReceiver, RemoteDrawReceiver, RemoteDrawPairing, RemoteDrawQRCode, lifecycle helpers.

Install

npm install @remotedraw/react-native
npm install react-native-svg@^15.15.4          # Expo: npx expo install react-native-svg
npm install --save-dev @babel/[email protected]

With pnpm, Yarn or bun: pnpm add / yarn add / bun add the same packages (-D for the Babel plugin). To use an unreleased build from a RemoteDraw checkout instead, see Working from a checkout.

Peers: react 19, react-native 0.81–0.87 (new architecture), react-native-svg 15. Dependencies: @remotedraw/client (capture controller, protocol), @remotedraw/geometry, qrcode-generator. No native module of its own, no WebView, no canvas.

Capture in 30 lines

import { useMemo } from "react";
import { View } from "react-native";
import { RemoteDrawCapture, createHttpSessionProvider, type CaptureResult } from "@remotedraw/react-native";

export function NoteDrawing({ noteId, token, save }: {
  noteId: string;
  token: string;
  save: (png: Uint8Array, width: number, height: number) => Promise<void>;
}) {
  // Your backend runs createSessionHandler (@remotedraw/server, Python, Go, .NET)
  // and holds the rd_sk_ key; the app only receives rd_recv_ tokens.
  const provider = useMemo(
    () => createHttpSessionProvider({
      endpoint: "https://example.com/api/remotedraw/session",
      headers: { Authorization: `Bearer ${token}` },
    }),
    [token],
  );
  return (
    <View style={{ padding: 16 }}>
      <RemoteDrawCapture
        surface={`note:${noteId}`}            // one capture per note; reopening adopts it
        provider={provider}
        onImage={async (image: CaptureResult) => save(image.png, image.width, image.height)}
        copy={{ "button.label": "Draw on phone" }}
      />
    </View>
  );
}

The button opens the pass (a Modal on phones, an inline popover on tablets and desktop); when the phone joins, the pass folds home and the live board appears under the button with the session island (undo, clear, insert, end). Insert exports the board and awaits onImage; if it throws, the island shows "Couldn't insert" with retry (the same bytes). After a successful insert the owned session ends (closeOnInsert, default true).

RemoteDrawCapture props

| Prop | Default | | |---|---|---| | provider | required unless controller | (SessionRequest) => Promise<SessionGrant>; createHttpSessionProvider({ endpoint, headers }) | | onImage | — | (CaptureResult) => void \| Promise<void>: { png, width, height, crop, revision, submissionId?, sessionId } | | surface | "default" | logical surface key: one controller per key per app (adoption) | | export | contract defaults | { crop: "ink" \| "board", padding, minPaddingPx, square, background, scale, maxPixels } | | closeOnInsert | true | end the owned session after a delivered insert | | closeOnDismiss | true | contract §5 dismiss rule; accepted for parity, see Dismissal | | label | copy button.label | button label (wins over copy["button.label"]) | | appearance | "label" | "label" or "icon" (36 pt mark, labelled for screen readers) | | afterConnect | "presence" | "presence" (green dot), "hide", "keep" | | actions | all four | subset of ["undo", "clear", "insert", "end"] | | theme | "auto" | "auto" (system), "light", "dark" | | accent | contract teal | colour | | background | "#ffffff" | board ground: a colour or your own view (under the ink) | | boardAspect | 4/3 | width / height until the session states its coordinate space | | controller | — | an external CaptureController (createCaptureController) | | presentation | "auto" | "modal", "popover" | | rasterize | null (server export) | local SVG → PNG, e.g. examples/skiaRasterize.ts | | copy | — | per-key copy overrides for this capture (see Copy) | | fontFamily | platform UI font | a bundled family (e.g. "Inter") | | reduceMotion | system | force reduced motion | | onPhaseChange, onEnded, onError, onOpenChange | — | events (onEnded only on real server termination) | | onCopy | — | ready-plate press with the join URL (wire expo-clipboard or @react-native-clipboard/clipboard) | | pauseInBackground | true | detach while the app is in the background |

The pieces are public too: RemoteDrawButton (provider or controller), PairingCode (controller or joinUrl + status), SessionStatus (controller, actions, alwaysOpen), RemoteDrawSurface (controller or drawings, background, boardAspect), RemoteDrawThemeProvider (theme, accent, copy, font for a subtree), RemoteDrawCopyProvider (copy only), useCaptureController, useCaptureState, and the framework-free createCaptureBinding / attachCaptureLifecycle.

Copy

Every visible and assistive string is a copy key (capture contract §7, appendix A; English in src/generated/design.ts COPY): labels, hints, accessible names, announcements, busy labels and error lines. Override any of them with RemoteDrawCapture's copy prop, or around standalone components with RemoteDrawCopyProvider (React's name; RemoteDrawThemeProvider takes copy too). Nested providers merge, the inner one winning per key; a component's own copy prop wins over providers; English fills the rest. An explicit input wins over the map for its string (label, accessibilityLabel). Placeholders are {name}.

<RemoteDrawCopyProvider copy={{ "button.label": "Teken op je telefoon", "error.generic": "Er ging iets mis." }}>
  <RemoteDrawCapture provider={provider} onImage={attach} copy={{ "status.insert": "Invoegen" }} />
</RemoteDrawCopyProvider>

Error lines show error.<code> for a code in CAPTURE_ERROR_CODES and error.generic otherwise (captureErrorCopyKey, re-exported here): the island tells a failed action as status.failedBecause ("Undo failed · …") and a failed insert as status.insertFailedBecause; the sheet's retry line shows the error's copy. The server's raw message never shows; it stays on CaptureError.message (onError). Only a host callback's own uncoded Error keeps its words.

Dismissal

closeOnDismiss (default true) is the contract §5 dismiss rule: when the user dismisses a presentation that holds the live board, an owned session is closed and a borrowed one detaches; false only detaches. This kit keeps the live board inline under the button, outside the pass's Modal (phones) or popover (tablets, desktop), as the React reference does. So dismissing the pass (backdrop tap, the button) never touches the session; a pass reopened to pair another phone closes over a live board. The prop is accepted for parity with the kits whose sheets hold the board. Unmount and the app going to the background only detach (below).

Lifecycle

  • One controller per surface per JS realm (acquireCaptureController); every view of a capture binds to it, nothing polls twice.
  • App to the background → detach() (polling stops, the session is kept; the server's 120 s receiver lease ends an abandoned owned session). Back to the foreground → attach().
  • Unmount → the view's lease is released; the last release detaches a live session so a remount with the same surface adopts it. Unmounting never ends a session: only close() (the island's End, or controller.close()) does.
  • An external controller follows the same rule: the last view showing it detaches it on unmount (or when the prop moves to another controller), and a view mounting it again re-attaches it, never a detach your code made.
  • Across process restarts the backend's logical create key (user + surface) returns the same live session.

When a session pauses, is rate-limited or ends

The capture controller from @remotedraw/client decides what a failed read means; onError gets a CaptureError with its disposition, code and message:

  • retry (too_many_requests, temporarily_unavailable, internal_error, request_timeout, network errors): the session and board stay, the controller polls again on its own and never before the server's Retry-After. The phase stays pairing or connected. Nothing to do.
  • paused (billing_limit_reached, wallet_balance_exhausted, billing_not_configured, included_capacity_exhausted, capability_not_granted, insufficient_scope, beta_access_required, sdk_too_old, other 4xx): the session is kept, reading stops, the phase is error and the board is hidden. The pass sheet's footer shows the error line (error.<code>, else error.generic) and a Retry (button.launch.error) that calls open(). Fix the cause, then call open().
  • terminal (invalid_receiver_token, session_not_found, 401/404/410): the session is over. onEnded fires when the server ended it (session_not_active, receiver_lease_expired, expired, ended); otherwise the phase is error. open() starts a new session.
  • operation (export_failed, delivery_failed, end_failed): the session and board are unaffected; the island tells it.

onError fires once per failure (a run of transient ones counts once) and not again after a resume. open() is the resume: it clears the failure at the call and reads once more, and is idempotent while a create or resume is in flight, so it is safe to repeat. Coming back from the background re-attaches a live capture but never lifts a pause; only open() does.

const capture = useCaptureController({
  provider,
  surface: `note:${noteId}`,
  onError: (error) => setPaused(error.disposition === "paused" ? error.code : null),
  onEnded: () => setPaused(null),
});
// …
<RemoteDrawCapture controller={capture} onImage={attach} />
{paused ? <Button title="Try again" onPress={() => { setPaused(null); void capture?.open(); }} /> : null}

Export

React Native has no canvas, so by default every insert is rendered by the server (POST /v1/receiver/export) with the same crop rules as local exports. Pass rasterize to export on the device (examples/skiaRasterize.ts uses @shopify/react-native-skia; it is not a dependency). A failing rasterize falls back to the server.

Visuals, motion, accessibility

Tokens, shadows (as react-native-svg blurred shapes), glyph primitives, the logo, pass geometry, curves and copy come from src/generated/design.ts (bun run design:generate writes it with the other kits' foundations). The pass's QR is module for module the web's (qrcode package segmentation and mask choice, built with qrcode-generator, level H). Animations use Animated with the contract's sampled curves; AccessibilityInfo's reduce motion setting turns them into short fades. Every control has an accessibilityRole and a label from the copy keys; island state changes are announced. On Windows, react-native-svg has no filters, so blurred shadow layers are left out.

Network

  • iOS ATS: RemoteDraw and your session endpoint are HTTPS; no exception needed. A plain-HTTP development backend on localhost needs NSAllowsLocalNetworking in the debug Info.plist only.
  • Kits reject rd_sk_ keys anywhere a credential is expected.

Expo

npx expo install react-native-svg, add the Babel plugin below to your Expo Babel config (keep babel-preset-expo), and clear the cache. react-native-svg is in Expo Go; the kit has no other native code. Expo bundling and runtime are unverified for this preview. Setup for Expo and bare apps: examples/README.md.

What is tested

bun test (Bun, no simulator): the capture conformance traces replayed through the controller in a React Native-shaped runtime (no DOM, no DOMException, whatwg-fetch-style responses, server export), QR equality with the web encoder over the fixture and 300 generated join URLs, component states and controller wiring (light renderer over React Native / react-native-svg shims), lifecycle (AppState detach/attach, unmount release, remount adoption), copy (every key a view reads is in the generated table; map, provider and explicit-input precedence; error lines from error copy, never raw messages), the dismiss rule (pass dismissal never touches the session), generated foundations. Parity shots render each manifest state's react-native-svg tree to SVG (Core Text label outlines) and rasterize it with resvg; see parity/render.ts and bun run parity. Not tested: any native runtime (iOS, Expo, Windows, macOS), real gestures and Modal behaviour, Skia export. Desktop: docs/desktop-spike.md.

Bare React Native Babel configuration

The shared protocol uses Zod 4's ESM namespace exports. With React Native 0.83.2's default Babel preset, Metro needs this Babel 7 transform:

npm install --save-dev @babel/[email protected]

Merge this plugin into your application's existing Babel configuration; keep its other presets and plugins. A complete bare React Native example is included at examples/babel.config.cjs:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['@babel/plugin-transform-export-namespace-from'],
};

Restart Metro with a cleared transform cache after changing configuration: npx react-native start --reset-cache. Keep the Babel 7-compatible version above when using this preset; the plugin's newer major requires Babel 8. Expo applications should retain their Expo preset and existing configuration. Expo bundling and runtime have not been verified by this preview.

import { useMemo } from "react";
import { View } from "react-native";
import {
  createNativeReceiver, RemoteDrawReceiver, RemoteDrawPairing,
  useReceiverLifecycle,
} from "@remotedraw/react-native";

export function DrawingScreen({ session }) {
  // session = {...response.session, receiverToken: response.receiverToken,
  //            joinUrl: response.joinUrl,
  //            joinTokenExpiresAt: response.joinTokenExpiresAt}
  const receiver = useMemo(() => createNativeReceiver({
    apiBaseUrl: "https://api.remotedraw.com", session,
  }), [session.id, session.receiverToken]);
  useReceiverLifecycle(receiver);
  return <View>
    <RemoteDrawPairing store={receiver} />
    <RemoteDrawReceiver store={receiver} />
  </View>;
}

Keep the rd_sk_... API key on your backend. Pass only scoped receiver credentials to the app. Use store.setSession(session) to replace pairing metadata or change boards. store.undo(), store.clear() and store.refreshJoinToken() expose receiver controls. stop() stops listening; end the server session explicitly through your backend when the workflow finishes.

Pairing includes a scannable native SVG QR by default, using the pure JavaScript [email protected] encoder with a four-module white quiet zone. No canvas, WebView, or additional native QR dependency is needed. renderQRCode={(url) => <YourQRCode value={url} />} optionally replaces the built-in QR. Only safe HTTPS URLs are displayed; expired tokens or ended/expired sessions hide the QR and link. Refresh pairing remains available during an active session to invite another phone. Pending actions are scoped to their originating session.

Development renderer scope

Freehand pressure/tilt path geometry, basic ink layers, shapes, points, text and HTTPS image drawings are supported. Aspect ratio comes from the session coordinate space; pass aspectRatio only when the host owns that mapping. Background content must occupy the same board rectangle. Hidden drawings are omitted. Physical material simulation is explicitly unsupported; advanced material compositing shows a basic vector fallback warning. Unknown tools and anchored annotations are reported by onUnsupported and a visible warning. No map navigation, phone viewport chrome, live screen streaming, DOM anchoring, advanced texture tooth/edge filters, or blend/flatten parity is claimed. The raw shared store remains available for custom renderers.

Validation

Automated validation for 0.1.0-dev.2 (2026-10-16, macOS, no simulator or device): the workspace build and typecheck pass against React Native 0.87.1 and react-native-svg 15.15.5 types; bun test runs 114 tests (1177 assertions) across 8 files, all passing (all 47 conformance traces of file version 7 (capture contract 1.4), QR equality and independent jsQR decoding, component states, copy and dismissal, lifecycle, foundations, and the 0.1.0-dev.1 receiver tests). The parity renderer (last run 2026-10-10) passes 22 of 23 manifest states against the React reference; button/open-ready still fails the content-box check by 3 device px (the popover sheet's wide shadow halo is about 2/727 darker at 1.2σ than the reference's, which moves the box edge across a flat plateau). The dev.1 Metro bundle check (Metro 0.83.8) has not been repeated for dev.2. Linux, r17 integration (2026-10-17): bun test 114 tests, all passing. Linux, r18 integration (2026-10-17): bun test 116 tests, all passing, including all 49 conformance traces of file version 10. None of this establishes native runtime or physical-device compatibility.

Run bun run build and bun run test. Install the package in your own Expo/RN app and test on iOS first: pairing, a long stroke, undo/clear, background/resume, expired pairing refresh, session replacement, and resizing. Expo's SVG package documentation: https://docs.expo.dev/versions/latest/sdk/svg/ ; lifecycle reference: https://reactnative.dev/docs/appstate .