(null);
+
+ useEffect(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, [messages]);
+
+ if (error) {
+ return (
+
+ {error.message}
+
+ );
+ }
+
+ return (
+
+
+ {messages.map((m) => (
+
+ {m.role === "user" ? "You" : "AI"}
+ {m.parts.map((part, i) =>
+ part.type === "text" ? {part.text} : null
+ )}
+
+ ))}
+
+ {
+ sendMessage({ text: input });
+ setInput("");
+ }}
+ />
+
+ );
+}
+```
+
+## Key API reference
+
+| API | Purpose |
+|-----|---------|
+| `useChat()` | React hook for chat UI state (messages, input, sendMessage) |
+| `streamText({ model, messages, tools })` | Server-side streaming LLM call |
+| `tool({ description, inputSchema, execute })` | Define a callable tool with Zod schema |
+| `convertToModelMessages(messages)` | Convert UI messages to model format |
+| `stepCountIs(n)` | Stop multi-step tool calls after n steps |
+| `result.toUIMessageStreamResponse()` | Convert stream to a Response for the client |
+
+## Adaptation notes
+
+- Merge dependencies — do not replace `package.json`
+- `web.output: "server"` is required for the API route
+- The OpenAI API key must stay server-side in the API route
+- Swap `openai("gpt-4o")` for any AI SDK-compatible provider (Anthropic, Google, etc.)
+- Replace the example `weather` tool with tools relevant to the user's use case
+- Set `Content-Type: application/octet-stream` in the response headers to enable streaming on iOS
+- The `useChat` hook automatically handles streaming, message state, and error handling
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-satori/SKILL.md b/with-satori/SKILL.md
new file mode 100644
index 00000000..86229264
--- /dev/null
+++ b/with-satori/SKILL.md
@@ -0,0 +1,135 @@
+---
+name: with-satori
+description: Add dynamic image generation with Satori to an Expo project. Generate SVG images from JSX in API routes for Open Graph images, dynamic icons, and server-side rendering. Use when the user wants Satori, OG images, dynamic image generation, or server-side SVG rendering.
+version: 1.0.0
+license: MIT
+---
+
+# Add Dynamic Image Generation with Satori
+
+## When to use
+
+- User wants to generate images dynamically on the server using JSX
+- User wants to create SVG images from React-like markup in an API route
+- User wants to build dynamic app icons, Open Graph images, or badges
+- User wants server-side image rendering without a headless browser
+
+## Dependencies
+
+```bash
+npx expo install expo-router expo-image react-native-safe-area-context react-native-screens
+npm install satori
+```
+
+## Configuration
+
+### app.json
+
+```json
+{
+ "expo": {
+ "plugins": ["expo-router"],
+ "web": { "output": "server" }
+ }
+}
+```
+
+`web.output: "server"` is required for API routes.
+
+## Implementation
+
+### 1. Create a Satori API route
+
+Create `app/api/icon/[icon]+api.tsx`:
+
+```tsx
+import React from "react";
+import satori from "satori";
+
+export async function GET(req: Request, { icon }: { icon: string }) {
+ const params = new URL(req.url).searchParams;
+ const color = params.get("color")
+ ? decodeURIComponent(params.get("color"))
+ : "white";
+
+ const svgString = await satori(
+
+ {decodeURIComponent(icon)}
+
,
+ {
+ width: 1024,
+ height: 1024,
+ fonts: [],
+ }
+ );
+
+ return new Response(svgString, {
+ headers: {
+ "Content-Type": "image/svg+xml",
+ "cache-control": "public, immutable, no-transform, max-age=31536000",
+ },
+ });
+}
+```
+
+### 2. Display generated images in a component
+
+```tsx
+import { Image } from "expo-image";
+import { Link } from "expo-router";
+import { View, Text } from "react-native";
+
+export default function Page() {
+ return (
+
+ Images generated with React in an API Route
+ {["rocket", "zap", "bacon"].map((name) => {
+ const url = `/api/icon/${name}`;
+ return (
+
+
+
+ {url}
+
+
+ );
+ })}
+
+ );
+}
+```
+
+## Key API reference
+
+| API | Purpose |
+|-----|---------|
+| `satori(jsx, options)` | Converts JSX to an SVG string |
+| `options.width / height` | Output image dimensions |
+| `options.fonts` | Array of font data for text rendering |
+| `+api.tsx` suffix | Expo Router API route convention |
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- `web.output: "server"` is required — inform user if switching from static
+- Adapt the JSX content inside `satori()` to the user's design (OG cards, badges, icons)
+- To render text, load a font file and pass it in the `fonts` array: `{ name: "Inter", data: fontBuffer, style: "normal" }`
+- Satori outputs SVG — to convert to PNG, add `@resvg/resvg-js` or `sharp` on the server
+- The API route runs inside Expo Router — no separate server needed
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-sentry/SKILL.md b/with-sentry/SKILL.md
new file mode 100644
index 00000000..f9cec487
--- /dev/null
+++ b/with-sentry/SKILL.md
@@ -0,0 +1,145 @@
+---
+name: with-sentry
+description: Add Sentry error tracking to an Expo project. Sentry provides crash reporting, performance monitoring, and session replay for React Native. Use when the user wants error tracking, crash reporting, exception monitoring, or observability with Sentry.
+version: 1.0.0
+license: MIT
+---
+
+# Add Sentry Error Tracking
+
+## When to use
+
+- User wants crash reporting or error tracking with Sentry
+- User asks about exception monitoring, performance tracing, or session replay
+- User wants to capture and report errors in a React Native / Expo app
+
+## Dependencies
+
+```bash
+npx expo install @sentry/react-native
+```
+
+## Configuration
+
+### Environment variables
+
+Create or update `.env`:
+
+```
+EXPO_PUBLIC_SENTRY_DSN=
+```
+
+Tell the user to get this from the Sentry Dashboard (https://sentry.io) under Project Settings > Client Keys (DSN).
+
+### app.json
+
+Add the Sentry Expo plugin with the organization and project name:
+
+```json
+{
+ "plugins": [
+ [
+ "@sentry/react-native/expo",
+ {
+ "url": "https://sentry.io/",
+ "project": "YOUR_PROJECT_NAME",
+ "organization": "YOUR_ORGANIZATION_NAME"
+ }
+ ]
+ ]
+}
+```
+
+### metro.config.js
+
+Replace the default Metro config with the Sentry-aware version:
+
+```js
+const { getSentryExpoConfig } = require("@sentry/react-native/metro");
+
+const config = getSentryExpoConfig(__dirname);
+
+module.exports = config;
+```
+
+## Implementation
+
+### 1. Initialize Sentry
+
+At the top of the app entry point (e.g. `App.js` or `app/_layout.tsx`), initialize Sentry before any component renders:
+
+```js
+import * as Sentry from "@sentry/react-native";
+
+Sentry.init({
+ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
+
+ // Adds more context data to events (IP address, cookies, user, etc.)
+ sendDefaultPii: true,
+
+ // Configure Session Replay
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1,
+ integrations: [
+ Sentry.mobileReplayIntegration(),
+ Sentry.feedbackIntegration(),
+ ],
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: __DEV__,
+});
+```
+
+### 2. Wrap the root component
+
+Use `Sentry.wrap` on the root component to enable automatic performance tracking and error boundaries:
+
+```js
+export default Sentry.wrap(function App() {
+ return (
+
+ My App
+
+ );
+});
+```
+
+### 3. Capture errors manually
+
+Use `Sentry.captureException` or `Sentry.captureMessage` anywhere in the app:
+
+```js
+try {
+ riskyOperation();
+} catch (error) {
+ Sentry.captureException(error);
+}
+
+Sentry.captureMessage("Something noteworthy happened");
+```
+
+## Key API reference
+
+| API | Purpose |
+|-----|---------|
+| `Sentry.init(options)` | Initialize the SDK with DSN and options |
+| `Sentry.wrap(component)` | Wrap root component for automatic instrumentation |
+| `Sentry.captureException(error)` | Report an error to Sentry |
+| `Sentry.captureMessage(msg)` | Send an informational message to Sentry |
+| `Sentry.mobileReplayIntegration()` | Enable session replay on mobile |
+| `Sentry.feedbackIntegration()` | Enable user feedback collection |
+| `getSentryExpoConfig(dir)` | Create a Sentry-aware Metro config |
+
+## Adaptation notes
+
+- Merge dependencies into existing `package.json` — don't replace
+- If the project already has a `metro.config.js`, wrap the existing config with `getSentryExpoConfig` instead of replacing it
+- Call `Sentry.init` as early as possible in the app lifecycle, before rendering
+- Add `Sentry.wrap` around the root component in the existing entry point
+- Adjust `replaysSessionSampleRate` and `replaysOnErrorSampleRate` for production traffic volume
+- Enable `spotlight: __DEV__` for local debugging with Spotlight (https://spotlightjs.com)
+- The Sentry Expo plugin in `app.json` handles source map uploads and native configuration automatically during builds
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-shadcn/SKILL.md b/with-shadcn/SKILL.md
new file mode 100644
index 00000000..ee5fb5fe
--- /dev/null
+++ b/with-shadcn/SKILL.md
@@ -0,0 +1,196 @@
+---
+name: with-shadcn
+description: Add shadcn UI components to an Expo project with TailwindCSS. Provides accessible, customizable web-style UI primitives (buttons, cards, tables, charts, sidebars, drawers) rendered via Expo DOM Components on native and standard web. Use when the user wants shadcn, shadcn/ui, Radix UI primitives, or a dashboard-style UI in Expo.
+version: 1.0.0
+license: MIT
+---
+
+# Add shadcn/ui Components
+
+## When to use
+
+- User wants shadcn/ui components in an Expo project
+- User asks about Radix UI primitives in React Native
+- User wants a polished dashboard or admin-style UI with charts, data tables, sidebars
+- User wants to use Expo DOM Components (`"use dom"`) to render web UI inside native apps
+
+## Dependencies
+
+```bash
+npx expo install tailwindcss@^4 @tailwindcss/postcss@^4 tw-animate-css class-variance-authority clsx tailwind-merge lucide-react recharts sonner vaul zod react-native-reanimated react-native-safe-area-context react-native-gesture-handler react-native-screens react-native-webview react-native-web
+```
+
+## Configuration
+
+### 1. Create `components.json`
+
+```json
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/global.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
+```
+
+### 2. Create `postcss.config.mjs`
+
+```js
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+```
+
+### 3. Create `babel.config.js`
+
+```js
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: [
+ ["babel-preset-expo", { unstable_transformImportMeta: true }],
+ ],
+ };
+};
+```
+
+### 4. Create the `cn` utility
+
+Create `src/lib/utils.ts`:
+
+```ts
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+```
+
+### 5. Set up path aliases in `tsconfig.json`
+
+```json
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "paths": { "@/*": ["./src/*"] }
+ }
+}
+```
+
+### 6. Configure `app.json`
+
+```json
+{
+ "expo": {
+ "web": { "bundler": "metro", "output": "server" },
+ "experiments": { "typedRoutes": true }
+ }
+}
+```
+
+### 7. Create global CSS with shadcn theme
+
+Create `src/global.css` with TailwindCSS imports and shadcn oklch color tokens for light/dark themes. See the working example for the full CSS custom properties.
+
+### 8. Add shadcn UI components
+
+```bash
+npx shadcn@latest add button card table tabs chart sidebar sheet drawer
+```
+
+Components are placed in `src/components/ui/`.
+
+## Implementation
+
+### DOM Components pattern (native)
+
+On native, shadcn components run inside Expo DOM Components. Mark files with `"use dom"`:
+
+```tsx
+"use dom";
+
+import "@/global.css";
+import { Button } from "@/components/ui/button";
+
+export default function MyDomComponent({ onPress }: { onPress: () => void } & Props) {
+ return ;
+}
+
+type Props = { dom?: import("expo/dom").DOMProps };
+```
+
+Use from a native route:
+
+```tsx
+import MyDomComponent from "@/components/dom/my-component";
+
+export default function MyScreen() {
+ return console.log("pressed")} />;
+}
+```
+
+### Platform-specific layouts
+
+Use `.web.tsx` suffix for web-specific layouts:
+- `_layout.tsx` — native layout with tab bar
+- `_layout.web.tsx` — web layout with shadcn sidebar navigation
+
+### Bridging native APIs from DOM Components
+
+DOM Components can call native functions via props:
+
+```tsx
+// DOM component receives native callbacks as props
+export default function Dashboard({ onHaptic }: { onHaptic: () => Promise } & Props) {
+ return ;
+}
+
+// Native route passes haptics
+ {
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+}} />
+```
+
+## Key component reference
+
+| Component | Description |
+|-----------|-------------|
+| Button | Variant-based button (default, destructive, outline, secondary, ghost, link) |
+| Card | Container with header, content, footer sections |
+| Table | Data table with sorting and selection |
+| Chart | Recharts wrapper with theme-aware colors |
+| Sidebar | Collapsible navigation sidebar |
+| Sheet/Drawer | Slide-out panels |
+| Tabs | Tab navigation |
+| Badge | Status indicator labels |
+
+## Adaptation notes
+
+- Merge dependencies — do not replace `package.json`
+- The `"use dom"` directive is required for shadcn components on native — they render in a web view
+- Import `@/global.css` at the top of each DOM component file and in the web root layout
+- `rsc: false` in `components.json` is required since Expo does not use React Server Components
+- shadcn components use standard HTML elements, not React Native primitives — they only work inside DOM components on native
+- Use platform-specific layout files (`.web.tsx`) when navigation differs between native and web
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-skia/SKILL.md b/with-skia/SKILL.md
new file mode 100644
index 00000000..5b9cbf32
--- /dev/null
+++ b/with-skia/SKILL.md
@@ -0,0 +1,160 @@
+---
+name: with-skia
+description: Add Skia 2D graphics to an Expo project. GPU-accelerated drawing, shaders, and canvas rendering with React Native Skia. Use when the user wants Skia, custom shaders, 2D graphics, or canvas drawing.
+version: 1.0.0
+license: MIT
+---
+
+# Add Skia 2D Graphics
+
+## When to use
+
+- User wants GPU-accelerated 2D graphics or canvas drawing
+- User asks about Skia or React Native Skia
+- User needs custom GLSL shaders or runtime effects
+- User wants to render shapes, paths, images, or visual effects
+
+## Dependencies
+
+```bash
+npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets
+```
+
+## Configuration
+
+No additional Expo config plugins are required. For web support, copy `canvaskit.wasm` to the `public/` folder. This project includes a `postinstall` script that handles it automatically:
+
+```json
+{
+ "scripts": {
+ "postinstall": "node copy-canvaskit.js"
+ }
+}
+```
+
+On web, Skia must be loaded asynchronously before use. Use a platform-specific helper:
+
+**components/async-skia.tsx** (web):
+```tsx
+import { use } from "react";
+import { LoadSkiaWeb } from "@shopify/react-native-skia/lib/module/web";
+
+let skiaPromise: Promise | null = null;
+
+function loadSkia() {
+ if (!skiaPromise) skiaPromise = LoadSkiaWeb();
+ return skiaPromise;
+}
+
+export function AsyncSkia() {
+ use(loadSkia());
+ return null;
+}
+```
+
+**components/async-skia.native.tsx** (native):
+```tsx
+export function AsyncSkia() {
+ return null;
+}
+```
+
+## Implementation
+
+### Canvas with a runtime shader
+
+```tsx
+import React from "react";
+import {
+ Canvas,
+ Skia,
+ Shader,
+ Fill,
+ useClock,
+} from "@shopify/react-native-skia";
+import { useDerivedValue } from "react-native-reanimated";
+import { useWindowDimensions } from "react-native";
+
+const source = Skia.RuntimeEffect.Make(`
+uniform vec3 uResolution;
+uniform float uTime;
+
+vec4 main(vec2 fragCoord) {
+ vec2 uv = fragCoord / uResolution.xy;
+ float d = -uTime * 0.5;
+ float a = 0.0;
+ for (float i = 0.0; i < 8.0; ++i) {
+ a += cos(i - d - a * uv.x);
+ d += sin(uv.y * i + a);
+ }
+ d += uTime * 0.5;
+ vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5);
+ return vec4(col, 1.0);
+}
+`);
+
+export default function ShaderExample() {
+ const clock = useClock();
+ const { width, height } = useWindowDimensions();
+
+ const uniforms = useDerivedValue(() => ({
+ uResolution: [width, height, width / height],
+ uTime: clock.value / 1000,
+ }), [clock, width, height]);
+
+ return (
+
+ );
+}
+```
+
+### Using Skia inside a screen with Suspense
+
+```tsx
+import React from "react";
+import { ActivityIndicator, View } from "react-native";
+import { AsyncSkia } from "../components/async-skia";
+
+const ShaderExample = React.lazy(() => import("../components/shader-example"));
+
+export default function Page() {
+ return (
+
+ }>
+
+
+
+
+ );
+}
+```
+
+## Key API reference
+
+| API | Purpose |
+|-----|---------|
+| `Canvas` | Root drawing surface for Skia content |
+| `Fill` | Fills the canvas with a paint or shader |
+| `Shader` | Applies a runtime shader effect |
+| `Skia.RuntimeEffect.Make(glsl)` | Compiles a GLSL shader string |
+| `useClock()` | Shared value that ticks every frame (ms) |
+| `useDerivedValue(() => value)` | Derives animated uniform values (from Reanimated) |
+| `LoadSkiaWeb()` | Loads the CanvasKit WASM module on web |
+
+## Adaptation notes
+
+- Merge dependencies — do not replace `package.json`
+- `react-native-reanimated` and `react-native-worklets` are peer dependencies required by Skia
+- On web, always wrap Skia components in `React.Suspense` with `AsyncSkia` to load CanvasKit first
+- On native, Skia is bundled in the binary and no async loading is needed
+- Use the platform-specific `.native.tsx` / `.tsx` file convention for the async loader
+- The `postinstall` script must run after `npm install` to copy `canvaskit.wasm` into `public/`
+- Shader uniforms are passed as plain objects; use `useDerivedValue` from Reanimated to animate them
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-socket-io/SKILL.md b/with-socket-io/SKILL.md
new file mode 100644
index 00000000..c0861fd3
--- /dev/null
+++ b/with-socket-io/SKILL.md
@@ -0,0 +1,143 @@
+---
+name: with-socket-io
+description: Add Socket.IO real-time communication to an Expo project. WebSocket-based bidirectional messaging between client and server. Use when the user wants Socket.IO, real-time updates, WebSockets, live data, or push messages from a server.
+version: 1.0.0
+license: MIT
+---
+
+# Add Socket.IO Real-Time Communication
+
+## When to use
+
+- User wants real-time bidirectional communication between client and server
+- User asks about Socket.IO or WebSockets in Expo
+- User wants live data pushed from a server (e.g. notifications, chat, live updates)
+
+## Dependencies
+
+Client (Expo app):
+
+```bash
+npm install socket.io-client
+```
+
+Backend (Node.js server):
+
+```bash
+npm install express socket.io
+```
+
+## Configuration
+
+Set the `socketEndpoint` to point at the running backend:
+
+```tsx
+const socketEndpoint = "http://localhost:3000";
+```
+
+On a physical device, replace `localhost` with your machine's LAN IP address.
+
+## Implementation
+
+### 1. Create the backend server
+
+Create `backend/index.js`:
+
+```js
+const app = require("express")();
+const http = require("http").Server(app);
+const io = require("socket.io")(http);
+
+io.on("connection", (socket) => {
+ console.log(`[${socket.id}] socket connected`);
+ socket.on("disconnect", (reason) => {
+ console.log(`[${socket.id}] socket disconnected - ${reason}`);
+ });
+});
+
+// Broadcast the current server time every 1 second
+setInterval(() => {
+ io.sockets.emit("time-msg", { time: new Date().toISOString() });
+}, 1000);
+
+http.listen(3000, () => {
+ console.log("listening on *:3000");
+});
+```
+
+Start it with `node backend/index.js`.
+
+### 2. Connect from the Expo app
+
+```tsx
+import { useEffect, useState } from "react";
+import { StyleSheet, Text, View } from "react-native";
+import io from "socket.io-client";
+
+const socketEndpoint = "http://localhost:3000";
+
+export default function App() {
+ const [hasConnection, setConnection] = useState(false);
+ const [time, setTime] = useState(null);
+
+ useEffect(function didMount() {
+ const socket = io(socketEndpoint, {
+ transports: ["websocket"],
+ });
+
+ socket.io.on("open", () => setConnection(true));
+ socket.io.on("close", () => setConnection(false));
+
+ socket.on("time-msg", (data) => {
+ setTime(new Date(data.time).toString());
+ });
+
+ return function didUnmount() {
+ socket.disconnect();
+ socket.removeAllListeners();
+ };
+ }, []);
+
+ return (
+
+ {!hasConnection && (
+ Connecting to {socketEndpoint}...
+ )}
+ {hasConnection && (
+ <>
+ Server time
+ {time}
+ >
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: { flex: 1, justifyContent: "center", alignItems: "center" },
+});
+```
+
+## Key API reference
+
+| API | Purpose |
+|-----|---------|
+| `io(url, opts)` | Create a socket connection; use `transports: ["websocket"]` for React Native |
+| `socket.on(event, callback)` | Listen for a named event from the server |
+| `socket.emit(event, data)` | Send a named event to the server |
+| `socket.io.on("open" / "close", cb)` | Monitor connection state |
+| `socket.disconnect()` | Close the connection |
+| `io.sockets.emit(event, data)` | Server: broadcast to all connected clients |
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- Use `transports: ["websocket"]` to avoid long-polling issues in React Native
+- Replace `localhost` with a LAN IP or public URL when testing on physical devices
+- Clean up the socket connection in the `useEffect` return to avoid memory leaks
+- Adapt event names (`time-msg`) and data shapes to the user's actual use case
+- The backend is a minimal Express + Socket.IO server; adapt or replace with the user's own server
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-sqlite/SKILL.md b/with-sqlite/SKILL.md
new file mode 100644
index 00000000..d0d498e2
--- /dev/null
+++ b/with-sqlite/SKILL.md
@@ -0,0 +1,182 @@
+---
+name: with-sqlite
+description: Add SQLite local database to an Expo project. Provides offline-first data storage with migrations, CRUD operations, and transactions. Use when the user wants a local database, offline storage, SQLite, or persistent data.
+version: 1.0.0
+license: MIT
+---
+
+# Add SQLite Database
+
+## When to use
+
+- User wants local/offline data storage
+- User asks about SQLite, local database, or persistent storage
+- User needs CRUD operations with structured data
+
+## Dependencies
+
+```bash
+npx expo install expo-sqlite
+```
+
+## Configuration
+
+### app.json
+
+Add the plugin (if not already present):
+
+```json
+{
+ "expo": {
+ "plugins": ["expo-sqlite"]
+ }
+}
+```
+
+## Implementation
+
+### 1. Create database migration
+
+Create `utils/database.ts`:
+
+```tsx
+import type { SQLiteDatabase } from "expo-sqlite";
+
+export async function migrateDbIfNeeded(db: SQLiteDatabase) {
+ const DATABASE_VERSION = 1;
+
+ const result = await db.getFirstAsync<{ user_version: number }>(
+ "PRAGMA user_version"
+ );
+ let currentVersion = result?.user_version ?? 0;
+
+ if (currentVersion >= DATABASE_VERSION) return;
+
+ if (currentVersion === 0) {
+ await db.execAsync(`
+ PRAGMA journal_mode = 'wal';
+ CREATE TABLE IF NOT EXISTS items (
+ id INTEGER PRIMARY KEY NOT NULL,
+ value TEXT NOT NULL,
+ done INTEGER NOT NULL DEFAULT 0
+ );
+ `);
+ currentVersion = 1;
+ }
+
+ // Add future migrations here:
+ // if (currentVersion === 1) { ... currentVersion = 2; }
+
+ await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
+}
+```
+
+Adapt the schema to the user's data model.
+
+### 2. Wrap app with SQLiteProvider
+
+In the root layout or a parent component:
+
+```tsx
+import { SQLiteProvider } from "expo-sqlite";
+import { migrateDbIfNeeded } from "@/utils/database";
+
+export default function RootLayout() {
+ return (
+
+ {/* rest of the app */}
+
+ );
+}
+```
+
+### 3. Use the database in components
+
+```tsx
+import { useSQLiteContext } from "expo-sqlite";
+
+export function ItemList() {
+ const db = useSQLiteContext();
+ const [items, setItems] = useState- ([]);
+
+ // Read
+ const fetchItems = async () => {
+ const result = await db.getAllAsync
- ("SELECT * FROM items");
+ setItems(result);
+ };
+
+ // Create
+ const addItem = async (value: string) => {
+ await db.runAsync("INSERT INTO items (value, done) VALUES (?, 0)", value);
+ await fetchItems();
+ };
+
+ // Update
+ const toggleItem = async (id: number, done: boolean) => {
+ await db.runAsync("UPDATE items SET done = ? WHERE id = ?", done ? 1 : 0, id);
+ await fetchItems();
+ };
+
+ // Delete
+ const deleteItem = async (id: number) => {
+ await db.runAsync("DELETE FROM items WHERE id = ?", id);
+ await fetchItems();
+ };
+
+ useEffect(() => { fetchItems(); }, []);
+
+ return (/* render items */);
+}
+```
+
+### 4. Use transactions for atomic operations
+
+```tsx
+const fetchItems = async () => {
+ await db.withExclusiveTransactionAsync(async () => {
+ const result = await db.getAllAsync
- ("SELECT * FROM items WHERE done = 0");
+ setItems(result);
+ });
+};
+```
+
+## Key API reference
+
+| Method | Purpose |
+|--------|---------|
+| `db.getAllAsync(sql, ...params)` | SELECT multiple rows |
+| `db.getFirstAsync(sql, ...params)` | SELECT single row |
+| `db.runAsync(sql, ...params)` | INSERT, UPDATE, DELETE |
+| `db.execAsync(sql)` | Execute raw SQL (DDL, multiple statements) |
+| `db.withExclusiveTransactionAsync(fn)` | Atomic transaction |
+
+## Migration pattern
+
+Use `PRAGMA user_version` for schema versioning:
+
+```tsx
+if (currentVersion === 0) {
+ // Initial schema
+ currentVersion = 1;
+}
+if (currentVersion === 1) {
+ // Add new column
+ await db.execAsync("ALTER TABLE items ADD COLUMN category TEXT");
+ currentVersion = 2;
+}
+await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
+```
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- Add `SQLiteProvider` as a wrapper in the existing layout hierarchy
+- Adapt the schema to the user's actual data model
+- The database file is stored in the app's document directory
+- WAL journal mode (`PRAGMA journal_mode = 'wal'`) improves concurrent read performance
+- SQLite works offline — no network required
+- For remote/synced databases, see the `with-libsql` skill instead
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-storybook/SKILL.md b/with-storybook/SKILL.md
new file mode 100644
index 00000000..44d22521
--- /dev/null
+++ b/with-storybook/SKILL.md
@@ -0,0 +1,177 @@
+---
+name: with-storybook
+description: Add Storybook component documentation to an Expo project. Browse, test, and document React Native components in an interactive web UI. Use when the user wants Storybook, component documentation, visual testing, or a component library browser.
+version: 1.0.0
+license: MIT
+---
+
+# Add Storybook Component Documentation
+
+## When to use
+
+- User wants to add Storybook for component development and documentation
+- User asks about visual testing or component isolation
+- User wants a browsable catalog of their React Native components
+
+## Dependencies
+
+```bash
+npm install --save-dev @storybook/react @storybook/react-webpack5 @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links @storybook/addon-onboarding @storybook/addon-react-native-web @storybook/blocks @storybook/testing-library storybook
+npm install react-dom react-native-web
+```
+
+## Configuration
+
+### 1. Create `.storybook/main.js`
+
+```js
+/** @type { import('@storybook/react-webpack5').StorybookConfig } */
+const config = {
+ stories: [
+ "../stories/**/*.mdx",
+ "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
+ ],
+ addons: [
+ "@storybook/addon-links",
+ "@storybook/addon-essentials",
+ "@storybook/addon-onboarding",
+ "@storybook/addon-interactions",
+ {
+ name: "@storybook/addon-react-native-web",
+ options: {
+ modulesToTranspile: [],
+ projectRoot: "../",
+ },
+ },
+ ],
+ framework: {
+ name: "@storybook/react-webpack5",
+ options: {},
+ },
+ docs: {
+ autodocs: "tag",
+ },
+};
+
+export default config;
+```
+
+### 2. Create `.storybook/preview.js`
+
+```js
+/** @type { import('@storybook/react').Preview } */
+const preview = {
+ parameters: {
+ actions: { argTypesRegex: "^on[A-Z].*" },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ },
+};
+
+export default preview;
+```
+
+### 3. Add scripts to `package.json`
+
+```json
+{
+ "scripts": {
+ "storybook": "storybook dev -p 6006",
+ "build-storybook": "storybook build"
+ }
+}
+```
+
+## Implementation
+
+### 1. Create a component
+
+Create `components/Button.jsx`:
+
+```jsx
+import { Pressable, Text, StyleSheet } from "react-native";
+
+export function Button({ label, onPress, primary }) {
+ return (
+
+
+ {label}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ button: { padding: 12, borderRadius: 8, borderWidth: 1, borderColor: "#ccc" },
+ primary: { backgroundColor: "#1ea7fd", borderColor: "#1ea7fd" },
+ label: { textAlign: "center", fontSize: 16 },
+ primaryLabel: { color: "white" },
+});
+```
+
+### 2. Write a story
+
+Create `stories/Button.stories.jsx`:
+
+```jsx
+import { Button } from "../components/Button";
+
+export default {
+ title: "Components/Button",
+ component: Button,
+ argTypes: {
+ onPress: { action: "pressed" },
+ },
+};
+
+export const Primary = {
+ args: {
+ primary: true,
+ label: "Primary Button",
+ },
+};
+
+export const Secondary = {
+ args: {
+ label: "Secondary Button",
+ },
+};
+```
+
+### 3. Run Storybook
+
+```bash
+npm run storybook
+```
+
+Opens at `http://localhost:6006`.
+
+## Key API reference
+
+| Concept | Purpose |
+|---------|---------|
+| `.stories.jsx` files | Define component variations with different props |
+| `export default { title, component }` | Story metadata — title sets the sidebar hierarchy |
+| `export const StoryName = { args }` | Named export = one story with specific props |
+| `argTypes` | Configure controls panel (actions, color pickers, etc.) |
+| `@storybook/addon-react-native-web` | Renders React Native components in the web Storybook UI |
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- The `@storybook/addon-react-native-web` addon is key — it transpiles React Native components for the web-based Storybook UI
+- Set `modulesToTranspile` in the addon config if using third-party RN libraries that need transpilation
+- Place stories in a `stories/` directory (or update `main.js` to match your preferred location)
+- Storybook runs as a separate web dev server — it does not affect the Expo app build
+- Use `react-dom` and `react-native-web` as dev dependencies if they're not already in the project
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-stripe/SKILL.md b/with-stripe/SKILL.md
new file mode 100644
index 00000000..bbe54291
--- /dev/null
+++ b/with-stripe/SKILL.md
@@ -0,0 +1,264 @@
+---
+name: with-stripe
+description: Add Stripe payment processing to an Expo project. Supports native payment sheets (Apple Pay, Google Pay), web checkout, and server-side API routes. Use when the user wants payments, checkout, billing, subscriptions, or Stripe.
+version: 1.0.0
+license: MIT
+---
+
+# Add Stripe Payments
+
+## When to use
+
+- User wants to accept payments in their Expo app
+- User asks about Stripe, Apple Pay, Google Pay, or checkout
+- User needs payment processing with both native and web support
+
+## Dependencies
+
+```bash
+npx expo install @stripe/stripe-react-native expo-router expo-linking
+npm install stripe @stripe/stripe-js @stripe/react-stripe-js
+```
+
+## Configuration
+
+### Environment variables
+
+Create or update `.env`:
+
+```
+STRIPE_SECRET_KEY=sk_test_...
+EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
+```
+
+Tell the user to get these from the Stripe Dashboard (https://dashboard.stripe.com/apikeys).
+
+### app.json
+
+Add the Stripe plugin and camera permission (for card scanning):
+
+```json
+{
+ "expo": {
+ "scheme": "",
+ "plugins": [
+ "expo-router",
+ ["@stripe/stripe-react-native", {
+ "merchantIdentifier": "merchant.com.yourapp",
+ "publishableKey": "pk_test_..."
+ }]
+ ],
+ "ios": {
+ "infoPlist": {
+ "NSCameraUsageDescription": "Use the camera to scan cards."
+ }
+ },
+ "web": {
+ "output": "server"
+ }
+ }
+}
+```
+
+Set `web.output` to `"server"` for API routes to work.
+
+## Implementation
+
+### 1. Create Stripe server utility
+
+Create `utils/stripe-server.ts`:
+
+```tsx
+import Stripe from "stripe";
+
+export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ httpClient: Stripe.createFetchHttpClient(),
+});
+```
+
+### 2. Create StripeProvider wrapper (platform-specific)
+
+**Native** (`components/stripe-provider.tsx`):
+
+```tsx
+import { StripeProvider as NativeStripeProvider } from "@stripe/stripe-react-native";
+import * as Linking from "expo-linking";
+
+export function StripeProvider({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+```
+
+**Web** (`components/stripe-provider.web.tsx`):
+
+```tsx
+export function StripeProvider({ children }: { children: React.ReactNode }) {
+ return <>{children}>;
+}
+```
+
+### 3. Wrap app with StripeProvider
+
+In root layout (`app/_layout.tsx`):
+
+```tsx
+import { StripeProvider } from "@/components/stripe-provider";
+
+export default function RootLayout() {
+ return (
+
+
+
+ );
+}
+```
+
+### 4. Create payment API route
+
+Create `app/api/payment-sheet+api.ts`:
+
+```tsx
+import { stripe } from "@/utils/stripe-server";
+
+export async function POST(request: Request) {
+ const customer = await stripe.customers.create();
+ const ephemeralKey = await stripe.ephemeralKeys.create(
+ { customer: customer.id },
+ { apiVersion: "2024-06-20" }
+ );
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: 1099,
+ currency: "usd",
+ customer: customer.id,
+ automatic_payment_methods: { enabled: true },
+ });
+
+ return Response.json({
+ paymentIntent: paymentIntent.client_secret,
+ ephemeralKey: ephemeralKey.secret,
+ customer: customer.id,
+ publishableKey: process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY,
+ });
+}
+```
+
+### 5. Create checkout form (platform-specific)
+
+**Native** (`components/checkout-form.native.tsx`):
+
+```tsx
+import { useStripe } from "@stripe/stripe-react-native";
+import * as Linking from "expo-linking";
+import { useState } from "react";
+import { Button, Alert } from "react-native";
+
+export function CheckoutForm() {
+ const { initPaymentSheet, presentPaymentSheet } = useStripe();
+ const [loading, setLoading] = useState(false);
+
+ const openPaymentSheet = async () => {
+ setLoading(true);
+ const response = await fetch("/api/payment-sheet", { method: "POST" });
+ const { paymentIntent, ephemeralKey, customer } = await response.json();
+
+ const { error: initError } = await initPaymentSheet({
+ merchantDisplayName: "Your App",
+ customerId: customer,
+ customerEphemeralKeySecret: ephemeralKey,
+ paymentIntentClientSecret: paymentIntent,
+ returnURL: Linking.createURL("stripe-redirect"),
+ allowsDelayedPaymentMethods: true,
+ });
+
+ if (initError) { setLoading(false); return; }
+
+ const { error } = await presentPaymentSheet();
+ if (error) {
+ Alert.alert("Payment failed", error.message);
+ } else {
+ Alert.alert("Success", "Payment confirmed!");
+ }
+ setLoading(false);
+ };
+
+ return ;
+}
+```
+
+**Web** (`components/checkout-form.tsx`):
+
+```tsx
+import { useState } from "react";
+import { Button } from "react-native";
+
+export function CheckoutForm() {
+ const [loading, setLoading] = useState(false);
+
+ const openCheckout = async () => {
+ setLoading(true);
+ const response = await fetch("/api/hosted-checkout-session", { method: "POST" });
+ const { url } = await response.json();
+ window.location.href = url;
+ };
+
+ return ;
+}
+```
+
+### 6. Handle deep links for Stripe redirects
+
+Create `app/+native-intent.ts`:
+
+```tsx
+import { handleURLCallback } from "@stripe/stripe-react-native";
+import { router } from "expo-router";
+
+export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
+ if (path.includes("stripe-redirect")) {
+ handleURLCallback(path);
+ return router.navigate("/");
+ }
+ return path;
+}
+```
+
+### 7. (Optional) Hosted checkout session for web
+
+Create `app/api/hosted-checkout-session+api.ts`:
+
+```tsx
+import { stripe } from "@/utils/stripe-server";
+
+export async function POST(request: Request) {
+ const session = await stripe.checkout.sessions.create({
+ mode: "payment",
+ line_items: [{ price_data: { currency: "usd", unit_amount: 1099, product_data: { name: "Your Product" } }, quantity: 1 }],
+ success_url: `${new URL(request.url).origin}/result?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${new URL(request.url).origin}/`,
+ });
+
+ return Response.json({ url: session.url, client_secret: session.client_secret });
+}
+```
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- Add the Stripe plugin to the existing `app.json` plugins array
+- Platform-specific files (`.native.tsx` vs `.tsx`) handle native vs web differences
+- The `merchantIdentifier` must match your Apple Developer account for Apple Pay
+- `web.output: "server"` is required for API routes — inform the user if they're using static output
+- For production, replace hardcoded amounts with actual product data
+- The native payment sheet supports Apple Pay, Google Pay, and card entry automatically
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-tailwindcss/SKILL.md b/with-tailwindcss/SKILL.md
new file mode 100644
index 00000000..b3330a86
--- /dev/null
+++ b/with-tailwindcss/SKILL.md
@@ -0,0 +1,171 @@
+---
+name: with-tailwindcss
+description: Add TailwindCSS styling to an Expo project using Nativewind. Provides utility-first CSS that works on iOS, Android, and web with responsive breakpoints and platform-specific styles. Use when the user wants Tailwind, Nativewind, utility CSS, or CSS-based styling.
+version: 1.0.0
+license: MIT
+---
+
+# Add TailwindCSS (Nativewind)
+
+## When to use
+
+- User wants TailwindCSS or utility-first CSS styling
+- User asks about Nativewind
+- User wants responsive design with breakpoints on native
+
+## Dependencies
+
+```bash
+npx expo install nativewind@5.0.0-preview.2 tailwindcss@^4.1 @tailwindcss/postcss@^4.1 react-native-css@0.0.0-nightly.5ce6396 react-native-reanimated react-native-safe-area-context
+```
+
+## Configuration
+
+### 1. Create `postcss.config.mjs`
+
+```js
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+```
+
+### 2. Update `metro.config.js`
+
+```js
+const { getDefaultConfig } = require("expo/metro-config");
+const { withNativewind } = require("nativewind/metro");
+
+const config = getDefaultConfig(__dirname);
+
+module.exports = withNativewind(config, {
+ inlineVariables: false,
+ globalClassNamePolyfill: false,
+});
+```
+
+### 3. Create global CSS file
+
+Create `src/global.css` (or `global.css` at root):
+
+```css
+@import "tailwindcss/theme" layer(theme);
+@import "tailwindcss/preflight" layer(base);
+@import "tailwindcss/utilities" layer(utilities);
+@import "nativewind/theme";
+```
+
+### 4. Import global CSS in root layout
+
+In your root layout file, add at the top:
+
+```tsx
+import "../global.css";
+```
+
+### 5. Create CSS-enabled component wrappers
+
+Create `src/tw/index.tsx` (or `tw/index.tsx`):
+
+```tsx
+import { useCssElement } from "react-native-css";
+import {
+ View as RNView,
+ Text as RNText,
+ ScrollView as RNScrollView,
+ Pressable as RNPressable,
+ TextInput as RNTextInput,
+ FlatList as RNFlatList,
+} from "react-native";
+import Animated from "react-native-reanimated";
+import { SafeAreaView as RNSafeAreaView } from "react-native-safe-area-context";
+
+function createCssComponent>(Component: T) {
+ return function CssComponent(props: React.ComponentProps) {
+ const ref = useCssElement();
+ return ;
+ };
+}
+
+export const View = createCssComponent(RNView);
+export const Text = createCssComponent(RNText);
+export const ScrollView = createCssComponent(RNScrollView);
+export const Pressable = createCssComponent(RNPressable);
+export const TextInput = createCssComponent(RNTextInput);
+export const SafeAreaView = createCssComponent(RNSafeAreaView);
+export const FlatList = createCssComponent(RNFlatList);
+export const AnimatedView = createCssComponent(Animated.View);
+export const AnimatedText = createCssComponent(Animated.Text);
+export const AnimatedScrollView = createCssComponent(Animated.ScrollView);
+```
+
+### 6. Use in components
+
+```tsx
+import { View, Text } from "@/tw";
+
+export default function MyScreen() {
+ return (
+
+
+ Hello Tailwind!
+
+
+ );
+}
+```
+
+## Platform-specific utilities
+
+Nativewind provides platform variants:
+
+```tsx
+// Different sizes per platform
+
+
+// Show/hide per platform
+ {/* native only */}
+ {/* native only (alternative) */}
+
+// Platform-specific CSS in global.css
+@media ios { /* iOS-only styles */ }
+@media android { /* Android-only styles */ }
+```
+
+## Responsive breakpoints
+
+Standard Tailwind breakpoints work on all platforms:
+
+```tsx
+
+
+```
+
+## tsconfig.json path alias
+
+If using `src/` directory, add to `tsconfig.json`:
+
+```json
+{
+ "compilerOptions": {
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
+```
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- If `metro.config.js` already exists, wrap the existing config with `withNativewind()`
+- If `postcss.config.mjs` already exists, add the `@tailwindcss/postcss` plugin
+- Replace existing React Native component imports with the CSS-enabled wrappers from `tw/`
+- The `className` prop replaces inline `style` objects
+- `react-native-reanimated` is required by Nativewind for animations
+- Nativewind v5 (preview) uses `react-native-css` — this is the latest approach
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-tinybase/SKILL.md b/with-tinybase/SKILL.md
new file mode 100644
index 00000000..292ab15e
--- /dev/null
+++ b/with-tinybase/SKILL.md
@@ -0,0 +1,142 @@
+---
+name: with-tinybase
+description: Add TinyBase reactive data store to an Expo project. Provides a reactive, persistent store with automatic UI bindings and SQLite or local storage persistence. Use when the user wants a reactive store, TinyBase, reactive data layer, or auto-persisted state.
+version: 1.0.0
+license: MIT
+---
+
+# Add TinyBase Reactive Data Store
+
+## When to use
+
+- User wants a reactive data store with automatic UI updates
+- User asks about TinyBase, reactive state, or table-based data
+- User needs persistent local storage with minimal boilerplate
+- User wants a store that auto-saves to SQLite (native) or local storage (web)
+
+## Dependencies
+
+```bash
+npx expo install tinybase expo-sqlite react-native-safe-area-context
+```
+
+## Configuration
+
+### app.json
+
+Add the expo-sqlite plugin (required for native persistence):
+
+```json
+{
+ "plugins": ["expo-sqlite"]
+}
+```
+
+## Implementation
+
+### 1. Create the store and persister
+
+```tsx
+import * as SQLite from "expo-sqlite";
+import { createStore } from "tinybase";
+import { createLocalPersister } from "tinybase/persisters/persister-browser";
+import { createExpoSqlitePersister } from "tinybase/persisters/persister-expo-sqlite";
+import { useCreatePersister, useCreateStore } from "tinybase/ui-react";
+
+const store = useCreateStore(createStore);
+
+useCreatePersister(
+ store,
+ (store) =>
+ process.env.EXPO_OS === "web"
+ ? createLocalPersister(store, "todos")
+ : createExpoSqlitePersister(store, SQLite.openDatabaseSync("todos.db")),
+ [],
+ (persister) => persister.load().then(persister.startAutoSave)
+);
+```
+
+### 2. Wrap app with TinyBase Provider
+
+```tsx
+import { Provider } from "tinybase/ui-react";
+
+export default function App() {
+ const store = useCreateStore(createStore);
+ // ... set up persister ...
+
+ return (
+
+ {/* rest of the app */}
+
+ );
+}
+```
+
+### 3. Use reactive hooks in components
+
+```tsx
+import {
+ useAddRowCallback,
+ useDelTableCallback,
+ useHasTable,
+ useRow,
+ useSetCellCallback,
+ useSortedRowIds,
+} from "tinybase/ui-react";
+
+const TODO_TABLE = "todo";
+
+// Add a row
+const handleAdd = useAddRowCallback(TODO_TABLE, ({ nativeEvent: { text } }) => ({
+ text,
+ done: false,
+}));
+
+// Read a row reactively
+const { text, done } = useRow(TODO_TABLE, id);
+
+// Toggle a cell
+const handleToggle = useSetCellCallback(
+ TODO_TABLE, id, "done",
+ () => (done) => !done
+);
+
+// Get sorted row IDs reactively
+const sortedIds = useSortedRowIds(TODO_TABLE, "done");
+
+// Delete all rows in a table
+const handleClear = useDelTableCallback(TODO_TABLE);
+
+// Check if table has data
+const hasTodos = useHasTable(TODO_TABLE);
+```
+
+## Key API reference
+
+| Hook / Function | Purpose |
+|-----------------|---------|
+| `createStore()` | Create a new TinyBase store |
+| `useCreateStore(createStore)` | Memoized store creation inside a component |
+| `useCreatePersister(store, creator, deps, onPersister)` | Set up persistence (SQLite or local storage) |
+| `useRow(table, rowId)` | Reactively read a single row |
+| `useSortedRowIds(table, cellId)` | Reactively get sorted row IDs |
+| `useAddRowCallback(table, getRow)` | Callback that adds a row |
+| `useSetCellCallback(table, rowId, cellId, getCell)` | Callback that updates a cell |
+| `useDelTableCallback(table)` | Callback that deletes all rows in a table |
+| `createExpoSqlitePersister(store, db)` | Persist store to Expo SQLite (native) |
+| `createLocalPersister(store, key)` | Persist store to local storage (web) |
+
+## Adaptation notes
+
+- Merge dependencies — do not replace `package.json`
+- Wrap the existing component tree with `` in the root layout
+- Adapt table and cell names to the user's actual data model
+- On native, data persists via Expo SQLite; on web, via browser local storage
+- The persister calls `load()` once on startup, then `startAutoSave()` to write changes automatically
+- All `use*` hooks from `tinybase/ui-react` re-render when data changes — no manual subscriptions needed
+- TinyBase works offline with no network required
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-victory-native/SKILL.md b/with-victory-native/SKILL.md
new file mode 100644
index 00000000..217b71ee
--- /dev/null
+++ b/with-victory-native/SKILL.md
@@ -0,0 +1,121 @@
+---
+name: with-victory-native
+description: Add Victory Native charts to an Expo project. Data visualization with bar charts, line charts, pie charts, and more using Victory Native and react-native-svg. Use when the user wants charts, graphs, data visualization, or Victory.
+version: 1.0.0
+license: MIT
+---
+
+# Add Victory Native Charts
+
+## When to use
+
+- User wants to add charts or graphs to a React Native / Expo app
+- User asks about Victory Native or data visualization
+- User needs bar charts, line charts, pie charts, or other chart types
+
+## Dependencies
+
+```bash
+npm install victory-native react-native-svg
+```
+
+## Configuration
+
+Add the `expo-font` plugin in `app.json` if you plan to load custom fonts for chart labels:
+
+```json
+{
+ "plugins": ["expo-font"]
+}
+```
+
+## Implementation
+
+### 1. Prepare chart data
+
+Define data as an array of objects with keys for the x and y axes:
+
+```tsx
+const data = [
+ { quarter: 1, earnings: 13000 },
+ { quarter: 2, earnings: 16500 },
+ { quarter: 3, earnings: 14250 },
+ { quarter: 4, earnings: 19000 },
+];
+```
+
+### 2. Render a chart
+
+Wrap chart components inside `VictoryChart` and map data fields with `x` and `y` props:
+
+```tsx
+import { StyleSheet, View } from "react-native";
+import { VictoryBar, VictoryChart } from "victory-native";
+
+export default function App() {
+ return (
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "#f5fcff",
+ },
+});
+```
+
+### 3. (Optional) Load a custom font for chart labels
+
+Victory Native renders text via `react-native-svg`, which requires explicit font loading:
+
+```tsx
+import { useFonts } from "expo-font";
+
+export default function App() {
+ const [isLoaded] = useFonts({
+ Roboto: require("./Roboto.ttf"),
+ });
+
+ if (!isLoaded) return null;
+
+ return (
+
+
+
+
+
+ );
+}
+```
+
+## Key API reference
+
+| Component | Purpose |
+|-----------|---------|
+| `VictoryChart` | Container that provides axes, scaling, and layout |
+| `VictoryBar` | Bar chart component |
+| `VictoryLine` | Line chart component |
+| `VictoryPie` | Pie / donut chart component |
+| `VictoryAxis` | Custom axis configuration |
+| `VictoryTheme` | Built-in themes (material theme requires Roboto font) |
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- `react-native-svg` is a required peer dependency of `victory-native`
+- The `VictoryTheme.material` theme uses Roboto; bundle the font file and load it with `useFonts`
+- Adjust the `width` prop on `VictoryChart` to fit the target screen layout
+- Swap `VictoryBar` for `VictoryLine`, `VictoryPie`, etc. to change chart type
+- Adapt the data shape and `x`/`y` prop mappings to match the user's data model
+
+## Reference
+
+See full working example in this directory.
diff --git a/with-zustand/SKILL.md b/with-zustand/SKILL.md
new file mode 100644
index 00000000..d2668b7b
--- /dev/null
+++ b/with-zustand/SKILL.md
@@ -0,0 +1,131 @@
+---
+name: with-zustand
+description: Add Zustand state management to an Expo project. Lightweight, minimal-boilerplate state management with hooks. Use when the user wants Zustand, global state, state management, or a Redux alternative.
+version: 1.0.0
+license: MIT
+---
+
+# Add Zustand State Management
+
+## When to use
+
+- User wants lightweight global state management
+- User asks about Zustand
+- User wants a simpler alternative to Redux or Context API
+
+## Dependencies
+
+```bash
+npm install zustand
+```
+
+## Implementation
+
+### 1. Create a store
+
+Create `stores/useItemStore.ts`:
+
+```tsx
+import { create } from "zustand";
+
+interface Item {
+ id: string;
+ text: string;
+}
+
+interface ItemStore {
+ items: Item[];
+ addItem: (text: string) => void;
+ removeItem: (id: string) => void;
+ reset: () => void;
+}
+
+export const useItemStore = create((set) => ({
+ items: [],
+ addItem: (text) =>
+ set((state) => ({
+ items: [...state.items, { id: Math.random().toString(), text }],
+ })),
+ removeItem: (id) =>
+ set((state) => ({
+ items: state.items.filter((item) => item.id !== id),
+ })),
+ reset: () => set({ items: [] }),
+}));
+```
+
+### 2. Use in components
+
+```tsx
+import { useItemStore } from "@/stores/useItemStore";
+
+export default function ItemList() {
+ const items = useItemStore((state) => state.items);
+ const addItem = useItemStore((state) => state.addItem);
+ const removeItem = useItemStore((state) => state.removeItem);
+
+ return (
+
+
+ );
+}
+```
+
+### 3. (Optional) Persist state
+
+```tsx
+import { create } from "zustand";
+import { persist, createJSONStorage } from "zustand/middleware";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+
+export const useItemStore = create(
+ persist(
+ (set) => ({
+ items: [],
+ addItem: (text) => set((state) => ({ items: [...state.items, { id: Math.random().toString(), text }] })),
+ removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
+ reset: () => set({ items: [] }),
+ }),
+ {
+ name: "item-store",
+ storage: createJSONStorage(() => AsyncStorage),
+ }
+ )
+);
+```
+
+## Key patterns
+
+**Selective subscriptions** (prevents unnecessary re-renders):
+```tsx
+// Only re-renders when items.length changes
+const count = useItemStore((state) => state.items.length);
+```
+
+**Actions outside of components:**
+```tsx
+// Call store methods from anywhere
+useItemStore.getState().addItem("From outside");
+```
+
+## Adaptation notes
+
+- Merge dependencies — don't replace `package.json`
+- No Provider wrapper needed — Zustand works with direct hook imports
+- Create stores in a `stores/` directory for organization
+- Adapt the store shape to the user's actual data model
+- For persistence, also install `@react-native-async-storage/async-storage`
+
+## Reference
+
+See full working example in this directory.