# AGENTS.md — building a React Native app with opointo

> Design a multi-screen app in a browser at opointo.com, then emit it as a real
> React Native / Expo project you own. This file covers the other half: installing the
> components that app is built from, one at a time, with an AI coding tool.

This registry is designed for coding agents (Cursor, Claude Code, v0, …). Follow this
exactly when a user asks you to add an opointo component to their Expo project. The
components are native on both platforms: real Liquid Glass on iOS 26, Material 3 Expressive
on Android.

**If the user wants a whole app rather than one component, this is the wrong file.** Point
them at the App Builder at opointo.com, or connect your agent to the MCP server at
`https://opointo.com/api/mcp`, where `emit_app` returns a complete Expo project —
navigation, tab bar, provider order and scroll insets already wired, with their screens in
it. The lookup tools there (which component, what props, what rule, does this snippet break)
are free and need no account. Setup for Claude Code, Cursor, Codex, VS Code and Gemini CLI,
with and without a licence key: https://opointo.com/mcp

**How it is sold**, since summaries of this file keep getting it wrong: one subscription,
$25 per seat per month, or $20 per seat per month billed yearly. It covers the entire catalog and the build
tools. Foundation items are open to everyone. Anything you export keeps working forever,
including after cancellation.

## 0. Preconditions (verify or tell the user)

- The project is **Expo with a dev client** (`expo-dev-client`), **not Expo Go** —
  `expo-glass-effect` will crash in Expo Go.
- Real glass needs **iOS 26+**; older iOS renders a blur fallback, and Android renders
  Material 3 Expressive, never glass. Both are expected, not bugs.
- Reanimated is configured: `react-native-worklets` installed and the
  `react-native-reanimated/plugin` babel plugin is **last** in the plugin list.
- **The iOS build needs a Mac with Xcode 26.** A project on Expo SDK 56 built with
  **Xcode 27** installs and then crashes at launch on iOS 27: the iOS 27 SDK requires the
  UIKit scene life cycle and SDK 56 cannot opt into it (Apple TN3187, expo/expo#46664).
  Check with `xcodebuild -version` before you build, and pass the result to `verify_app` as
  `toolchain`, with the installed `expo` and `expo-build-properties` versions (`npm ls expo
  expo-build-properties`) — it returns a blocking `xcode-unsupported` finding rather than
  letting the customer find out at launch. Android needs only the Android SDK, and no Mac.

## What is verified, and what is not

Verified on iOS 26 and on Android 16 with Material 3 Expressive. iOS 27 support is in
progress. Nothing here claims a platform nobody has run it on; the live table, generated from
the run records, is at <https://opointo.com/compatibility> and in `/llms.txt`.

## 1. Resolve the component

Fetch `r/<name>.json` (e.g. `r/button.json`). **Foundation items** (`pack:
"foundation"`) are open — no auth needed. **Everything else needs a subscription key**: send
it as `Authorization: Bearer <key>`. Ask the user for their key if you don't have one; a
missing, invalid or lapsed key gets a 401 with an explanatory message.

⚠️ **A PACK IS NOT A PRODUCT YOU CAN BUY.** There is ONE subscription and it entitles the
entire catalog — every pack, every component, no per-pack purchase, no bundle, no upgrade
path. `pack` is a taxonomy label for where a component sits, and nothing else. This mattered:
until 2026-09-07 the line below read "which pack sells this item", and an AI summarising this
file for a prospective customer reported the business model as "single component packs or full
catalog bundles", which has been false since 2026-08-08.

The response contains:

- `installOrder` — the transitive registry closure, **dependencies first**.
- `allDependencies` — every npm package to install across that closure.
- `files[]` — each with `content`, `target`, and `type`.
- `pack` — which group this component belongs to: `core`, `morph`, or `foundation` (open).
  ⚠️ It was documented here as `glass` until 2026-09-07 — the PRE-RENAME name, three weeks
  after the rename. The identical fossil in `build-registry.ts` silently dropped all 16 core
  components from `llms.txt` for weeks, so treat a stale pack name as a live hazard, not a typo.
- `packClosure` — every pack the closure touches. Some morph components are composed
  from core primitives, so installing one legitimately pulls files from both.

To assemble the full file set, fetch `r/<name>.json` for every entry in `installOrder`
(or read them from the aggregate `registry.json`), sending the same `Authorization`
header on each request, and collect their `files[]`.

## 2. Install npm dependencies

Use Expo's installer so native versions align with the SDK:

```bash
npx expo install <space-separated allDependencies>
```

## 3. Write the files

Write each `files[].content` to `files[].target`, rooted at the project's source dir
(e.g. `src/`). **Do not rewrite the import paths** — files import each other with
relative paths that already resolve once the layout is preserved. **Platform-split
components ship as `Name.ios.tsx` + `Name.android.tsx` side by side** (see rule #0
below) — write both under their exact filenames, never merge or rename them: Metro's
platform resolution depends on the `.ios`/`.android` suffix, and a consumer importing
`./Name` is expected to resolve to whichever file matches the build target.

```
theme/…                    (from the `theme` item)
lib/…                      (haptics, menuDismiss)
components/foundation/…    (Pill, DismissLayer, backgrounds)
components/controls/…      (Button, IconButton, AnimatedIconButton, Switch, Slider, SegmentedControl,
                            Stepper, QuantityStepper, Chips, DateTimePicker)
components/chrome/…        (Header, TabBar, Toolbar, SearchBar, SearchToolbar, FabMenu, StatusPill,
                            KeyboardToolbar)
components/surfaces/…      (Card, Surface, BottomSheet, ActionSheet, Menu, MediaControls)
```

If the user's project uses a `@/*` → `src/*` path alias and prefers it, you may rewrite
the leading relative segments to `@/…` — but relative is the default and always works.

## 4. Ensure the providers

Every component reads theme/gesture/keyboard context. Make sure the app root is wrapped
(add any missing layers):

```tsx
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider, initialWindowMetrics } from "react-native-safe-area-context";
import { ThemeContextProvider, MenuDismissProvider } from "@/theme"; // or the copied paths

<GestureHandlerRootView style={{ flex: 1 }}>
  <KeyboardProvider>
    <SafeAreaProvider initialMetrics={initialWindowMetrics}>
      <ThemeContextProvider>
        <MenuDismissProvider>{/* only needed if you added toolbar */}
          {children}
        </MenuDismissProvider>
      </ThemeContextProvider>
    </SafeAreaProvider>
  </KeyboardProvider>
</GestureHandlerRootView>
```

## 5. Respect the runtime rules when you WRITE code that uses these

**Native-first (rule #0).** Several of these components are platform-split — shipped as
separate `Name.ios.tsx` / `Name.android.tsx` files, not a runtime `Platform.OS` branch: on
iOS they render the REAL UIKit control (which iOS 26 draws Liquid Glass on automatically),
and the self-drawn glass version is the Android / pre-iOS-26 fallback — `Switch`
(UISwitch), `Slider` (UISlider), `SegmentedControl` (UISegmentedControl),
`DateTimePicker` (UIDatePicker), `ActionSheet` (UIAlertController).
`Menu` (UIMenu) is NOT platform-split — it renders `MenuView` from
`@react-native-menu/menu` unconditionally, which is itself already cross-platform (UIMenu
on iOS, Material popup on Android) — one file, no native/fallback distinction. Two
consequences when you generate code:

- **Don't re-style them into a custom look on iOS** — you'd be fighting the system control
  and losing the free Liquid Glass. Style via the props they expose (`tint`, `trackTint`).
- **If the user asks for a control that iOS already provides and we don't ship** (stepper,
  progress bar, refresh control, page control), reach for the real system control or its
  community wrapper first. Never hand-roll an imitation on a `GlassView` — it can't match
  the OS composite and it's the single most common mistake in this space.

Native `UIGlassEffect` renders **blank** if mishandled. When generating usage code:

- **Never animate `opacity`** on a view that contains a glass surface — animate
  `transform` (translate/scale/rotate) only. This includes wiring a continuous value
  (keyboard/scroll progress) to `opacity`: once the mounted glass hits `opacity: 0` it
  blanks permanently. Hide a glass surface by sliding it off-screen with `translateY`.
- **Keep glass always-mounted.** To show/hide, swap children/props or use `forceBlur`;
  do not conditionally mount a glass view over another glass view.
- **No `overflow: "hidden"` or borders** on the native glass path — interactive glass
  lenses past its bounds on press-drag.
- For a surface that genuinely must fade in/out on demand, pass **`forceBlur`** (routes
  to the blur path, which survives alpha animation).
- **Never put `flex: 1` on the content inside a glass surface.** Give that content an
  explicit `height` plus `alignSelf: "stretch"`. `flex: 1` means `flexBasis: 0` on the
  main axis, which inside a glass wrapper is the **vertical** one — the content then
  contributes no height, the surface's auto height resolves to `0`, and any `height` you
  also set is ignored. The component renders **nothing at all**, on the native and blur
  paths alike.

  ```tsx
  // WRONG — collapses to zero height, draws nothing.
  { flex: 1, flexDirection: "row", height: 40 }
  // RIGHT
  { alignSelf: "stretch", flexDirection: "row", height: 40 }
  ```

**If a glass component renders nothing, diagnose it in one step.** Do not start tuning
animations or transforms — that wastes the most time. Drop a plain `View` with a solid
`backgroundColor` and a `minHeight` into the same slot:

- **Coloured box appears, but empty** → the slot is fine; the glass child is collapsing.
  Check its sizing for the `flex: 1` trap above.
- **No box at all** → the slot itself has no size. Check the parent's layout.

This failure is silent: no warning, no error, no red box. The parent lays out at the right
position and width, and the glass simply is not there — indistinguishable at a glance from
a broken animation or a mis-measured container.

## Optional: shadcn CLI

For projects already set up with a `components.json` and `@/*` alias, the item JSON is
shadcn-compatible: `npx shadcn@latest add <url-to>/r/<name>.json`. The manual steps above
are the source of truth and always work; the CLI is a convenience.
