{"owner":"vercel-labs","repo":"json-render","hasSkills":true,"totalSkillsCount":99,"totalTokensCount":101990,"categories":["mcp-config","root-instruction","plugin-manifest","anthropic-skill"],"hasMcp":true,"mcpConfig":null,"found":[".cursor/mcp.json","AGENTS.md","examples/devtools/README.md","packages/codegen/README.md","packages/core/README.md","packages/devtools-react/README.md","packages/devtools-solid/README.md","packages/devtools-svelte/README.md","packages/devtools-vue/README.md","packages/devtools/README.md","packages/directives/README.md","packages/eslint-config/README.md","packages/image/README.md","packages/ink/README.md","packages/jotai/README.md","packages/mcp/README.md","packages/next/README.md","packages/react-email/README.md","packages/react-native/README.md","packages/react-pdf/README.md","packages/react-three-fiber/README.md","packages/react/README.md","packages/redux/README.md","packages/remotion/README.md","packages/shadcn-svelte/README.md","packages/shadcn/README.md","packages/solid/README.md","packages/svelte/README.md","packages/tanstack-start/README.md","packages/vue/README.md","packages/xstate/README.md","packages/yaml/README.md","packages/zustand/README.md","skills/codegen/SKILL.md","skills/core/SKILL.md","skills/devtools-react/SKILL.md","skills/devtools-solid/SKILL.md","skills/devtools-svelte/SKILL.md","skills/devtools-vue/SKILL.md","skills/devtools/SKILL.md","skills/directives/SKILL.md","skills/image/SKILL.md","skills/ink/SKILL.md","skills/jotai/SKILL.md","skills/mcp/SKILL.md","skills/next/SKILL.md","skills/react-email/SKILL.md","skills/react-native/SKILL.md","skills/react-pdf/SKILL.md","skills/react-three-fiber/SKILL.md","skills/react/SKILL.md","skills/redux/SKILL.md","skills/remotion-best-practices/SKILL.md","skills/remotion-best-practices/rules/3d.md","skills/remotion-best-practices/rules/animations.md","skills/remotion-best-practices/rules/assets.md","skills/remotion-best-practices/rules/audio.md","skills/remotion-best-practices/rules/calculate-metadata.md","skills/remotion-best-practices/rules/can-decode.md","skills/remotion-best-practices/rules/charts.md","skills/remotion-best-practices/rules/compositions.md","skills/remotion-best-practices/rules/display-captions.md","skills/remotion-best-practices/rules/extract-frames.md","skills/remotion-best-practices/rules/fonts.md","skills/remotion-best-practices/rules/get-audio-duration.md","skills/remotion-best-practices/rules/get-video-dimensions.md","skills/remotion-best-practices/rules/get-video-duration.md","skills/remotion-best-practices/rules/gifs.md","skills/remotion-best-practices/rules/images.md","skills/remotion-best-practices/rules/import-srt-captions.md","skills/remotion-best-practices/rules/light-leaks.md","skills/remotion-best-practices/rules/lottie.md","skills/remotion-best-practices/rules/maps.md","skills/remotion-best-practices/rules/measuring-dom-nodes.md","skills/remotion-best-practices/rules/measuring-text.md","skills/remotion-best-practices/rules/parameters.md","skills/remotion-best-practices/rules/sequencing.md","skills/remotion-best-practices/rules/subtitles.md","skills/remotion-best-practices/rules/tailwind.md","skills/remotion-best-practices/rules/text-animations.md","skills/remotion-best-practices/rules/timing.md","skills/remotion-best-practices/rules/transcribe-captions.md","skills/remotion-best-practices/rules/transitions.md","skills/remotion-best-practices/rules/transparent-videos.md","skills/remotion-best-practices/rules/trimming.md","skills/remotion-best-practices/rules/videos.md","skills/remotion/SKILL.md","skills/shadcn-svelte/SKILL.md","skills/shadcn/SKILL.md","skills/skill-creator/SKILL.md","skills/skill-creator/references/output-patterns.md","skills/skill-creator/references/workflows.md","skills/solid/SKILL.md","skills/svelte/SKILL.md","skills/tanstack-start/SKILL.md","skills/vue/SKILL.md","skills/xstate/SKILL.md","skills/yaml/SKILL.md","skills/zustand/SKILL.md"],"skills":{".cursor/mcp.json":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=.cursor%2Fmcp.json -->","AGENTS.md":"# AGENTS.md\n\nInstructions for AI coding agents working with this codebase.\n\n## Package Management\n\n**Always check the latest version before installing a package.**\n\nBefore adding or updating any dependency, verify the current latest version on npm:\n\n```bash\nnpm view <package-name> version\n```\n\nOr check multiple packages at once:\n\n```bash\nnpm view ai version\nnpm view @ai-sdk/provider-utils version\nnpm view zod version\n```\n\nThis ensures we don't install outdated versions that may have incompatible types or missing features.\n\n## Code Style\n\n- Do not use emojis in code or UI\n- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`\n- **Web app docs (`apps/web/`):** Never use Markdown table syntax (`| col | col |`). Always use HTML `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`. Markdown tables do not render correctly in the web app. Inside HTML table cells, curly braces must be escaped as JSX expressions (e.g. `<code>{'{ \"$state\": \"/path\" }'}</code>`) because MDX parses `{` as a JSX expression boundary.\n\n## AI SDK / AI Gateway\n\nWhen using the Vercel AI SDK (`ai` package) with AI Gateway, pass the model as a plain string identifier -- do not import a provider constructor:\n\n```ts\nimport { streamText } from \"ai\";\n\nconst result = streamText({\n  model: \"anthropic/claude-haiku-4.5\",\n  prompt: \"...\",\n});\n```\n\nThis requires `AI_GATEWAY_API_KEY` to be set in the environment. See `tests/e2e/` for examples.\n\n## Dev Servers\n\nAll apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.\n\nNaming convention:\n- Main web app: `json-render` → `json-render.localhost:1355`\n- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`\n\nWhen adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless my-example-demo.json-render next dev --turbopack\"\n  }\n}\n```\n\nDo **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.\n\n## Workflow\n\n- Run `pnpm type-check` after each turn to ensure type safety\n- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync.\n- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content.\n- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:\n  - Package `README.md` files in `packages/*/README.md`\n  - Root `README.md` (if packages table, install commands, or examples are affected)\n  - Web app docs in `apps/web/` (if guides, API references, or examples need updating)\n  - Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)\n  - `AGENTS.md` (if workflow or conventions change)\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nAll public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.\n\n### Preparing a release\n\nWhen asked to prepare a release (e.g. \"prepare v0.17.0\"):\n\n1. Create a branch (e.g. `prepare-v0.17.0`)\n2. Bump the version in `packages/core/package.json`\n3. Run `pnpm run version:sync` to update all other `@json-render/*` packages\n4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)\n5. **Fill documentation gaps** — every public package should have:\n   - A row in the root `README.md` packages table\n   - A renderer section in the root `README.md` (if it's a renderer)\n   - An API reference page at `apps/web/content/docs/api/<name>.mdx`\n   - An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`\n   - An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)\n   - A skill at `skills/<name>/SKILL.md`\n   - A `packages/<name>/README.md`\n6. **Run `pnpm type-check`** after all changes to verify nothing is broken\n7. Open a PR and merge to `main`\n\nCI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n### Scripts\n\n- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`\n- `pnpm run version:check` — verify all versions are in sync (runs in CI)\n- `pnpm run ci:publish` — build all packages and publish to npm (CI only)\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","examples/devtools/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=examples%2Fdevtools%2FREADME.md -->","packages/codegen/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcodegen%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcore%2FREADME.md -->","packages/devtools-react/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-react%2FREADME.md -->","packages/devtools-solid/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-solid%2FREADME.md -->","packages/devtools-svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-svelte%2FREADME.md -->","packages/devtools-vue/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-vue%2FREADME.md -->","packages/devtools/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools%2FREADME.md -->","packages/directives/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdirectives%2FREADME.md -->","packages/eslint-config/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Feslint-config%2FREADME.md -->","packages/image/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fimage%2FREADME.md -->","packages/ink/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fink%2FREADME.md -->","packages/jotai/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fjotai%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fmcp%2FREADME.md -->","packages/next/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fnext%2FREADME.md -->","packages/react-email/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-email%2FREADME.md -->","packages/react-native/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-native%2FREADME.md -->","packages/react-pdf/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-pdf%2FREADME.md -->","packages/react-three-fiber/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-three-fiber%2FREADME.md -->","packages/react/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact%2FREADME.md -->","packages/redux/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fredux%2FREADME.md -->","packages/remotion/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fremotion%2FREADME.md -->","packages/shadcn-svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn-svelte%2FREADME.md -->","packages/shadcn/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn%2FREADME.md -->","packages/solid/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsolid%2FREADME.md -->","packages/svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsvelte%2FREADME.md -->","packages/tanstack-start/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Ftanstack-start%2FREADME.md -->","packages/vue/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fvue%2FREADME.md -->","packages/xstate/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fxstate%2FREADME.md -->","packages/yaml/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fyaml%2FREADME.md -->","packages/zustand/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fzustand%2FREADME.md -->","skills/codegen/SKILL.md":"---\nname: codegen\ndescription: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen.\n---\n\n# @json-render/codegen\n\nFramework-agnostic utilities for generating code from json-render UI trees. Use these to build custom code exporters for Next.js, Remix, or other frameworks.\n\n## Installation\n\n```bash\nnpm install @json-render/codegen\n```\n\n## Tree Traversal\n\n```typescript\nimport {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n} from \"@json-render/codegen\";\n\n// Walk the spec depth-first\ntraverseSpec(spec, (element, key, depth, parent) => {\n  console.log(`${\" \".repeat(depth * 2)}${key}: ${element.type}`);\n});\n\n// Get all component types used\nconst components = collectUsedComponents(spec);\n// Set { \"Card\", \"Metric\", \"Button\" }\n\n// Get all state paths referenced\nconst statePaths = collectStatePaths(spec);\n// Set { \"analytics/revenue\", \"user/name\" }\n\n// Get all action names\nconst actions = collectActions(spec);\n// Set { \"submit_form\", \"refresh_data\" }\n```\n\n## Serialization\n\n```typescript\nimport {\n  serializePropValue,\n  serializeProps,\n  escapeString,\n  type SerializeOptions,\n} from \"@json-render/codegen\";\n\n// Serialize a single value\nserializePropValue(\"hello\");\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue({ $state: \"/user/name\" });\n// { value: '{ $state: \"/user/name\" }', needsBraces: true }\n\n// Serialize props for JSX\nserializeProps({ title: \"Dashboard\", columns: 3, disabled: true });\n// 'title=\"Dashboard\" columns={3} disabled'\n\n// Escape strings for code\nescapeString('hello \"world\"');\n// 'hello \\\"world\\\"'\n```\n\n### SerializeOptions\n\n```typescript\ninterface SerializeOptions {\n  quotes?: \"single\" | \"double\";\n  indent?: number;\n}\n```\n\n## Types\n\n```typescript\nimport type { GeneratedFile, CodeGenerator } from \"@json-render/codegen\";\n\nconst myGenerator: CodeGenerator = {\n  generate(spec) {\n    return [\n      { path: \"package.json\", content: \"...\" },\n      { path: \"app/page.tsx\", content: \"...\" },\n    ];\n  },\n};\n```\n\n## Building a Custom Generator\n\n```typescript\nimport {\n  collectUsedComponents,\n  collectStatePaths,\n  traverseSpec,\n  serializeProps,\n  type GeneratedFile,\n} from \"@json-render/codegen\";\nimport type { Spec } from \"@json-render/core\";\n\nexport function generateNextJSProject(spec: Spec): GeneratedFile[] {\n  const files: GeneratedFile[] = [];\n  const components = collectUsedComponents(spec);\n  // Generate package.json, component files, main page...\n  return files;\n}\n```\n","skills/core/SKILL.md":"---\nname: core\ndescription: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.\n---\n\n# @json-render/core\n\nCore package for schema definition, catalog creation, and spec streaming.\n\n## Key Concepts\n\n- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)\n- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)\n- **Spec**: JSON output from AI that conforms to the schema\n- **SpecStream**: JSONL streaming format for progressive spec building\n\n## Experimental Decision-Model Composition\n\nFor decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.\n\n- Run the Gateway evaluator server-side with `{ model: \"typesafe-ai/jev\", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.\n- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.\n- New trees default to `strategy: \"batch\"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: \"sequential\"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.\n- Batched trace steps use `choice: \"select\" | \"layout\"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.\n- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.\n- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.\n- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.\n- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.\n- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.\n- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.\n- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.\n- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.\n\nSee `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.\n\n## Defining a Schema\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nexport const schema = defineSchema((s) => ({\n  spec: s.object({\n    // Define spec structure\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate, // Optional custom AI prompt\n});\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"./schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button component\",\n    },\n  },\n});\n```\n\n## Generating AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt(); // Uses schema's promptTemplate\nconst systemPrompt = catalog.prompt({ customRules: [\"Rule 1\", \"Rule 2\"] });\n```\n\n## SpecStream Utilities\n\nFor streaming AI responses (JSONL patches):\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process streaming chunks\nconst { result, newPatches } = compiler.push(chunk);\n\n// Get final result\nconst finalSpec = compiler.getResult();\n```\n\n## Form Values in Action Handlers\n\nUse `findFormValue(\"email\", params, state)` to read a direct parameter, a dotted parameter key (such as `\"form.email\"`), a matching flat state key, or a slash-delimited path (such as `\"/form/email\"`) in nested state. Parameter values are literal, so emails and URLs containing dots are preserved. For action bindings that read nested state, use `{ $state: \"/form/email\" }`; the resolver passes that value to the handler. A bare `\"email\"` field name does not search nested `state.form.email`.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads a value from the state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - evaluates a visibility condition and picks a branch\n- **`{ \"$template\": \"Hello, ${/user/name}!\" }`** - interpolates `${/path}` references with state values\n- **`{ \"$computed\": \"fnName\", \"args\": { \"key\": <expression> } }`** - calls a registered function with resolved args\n\n`$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive).\n\nComponents do not use a `statePath` prop for two-way binding. Instead, use `{ \"$bindState\": \"/path\" }` on the natural value prop (e.g. `value`, `checked`, `pressed`).\n\n```json\n{\n  \"color\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"#007AFF\",\n    \"$else\": \"#8E8E93\"\n  },\n  \"label\": { \"$template\": \"Welcome, ${/user/name}!\" },\n  \"fullName\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\n```typescript\nimport { resolvePropValue, resolveElementProps } from \"@json-render/core\";\n\nconst resolved = resolveElementProps(element.props, { stateModel: myState });\n```\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": { \"value\": { \"$bindState\": \"/form/country\" }, \"options\": [\"US\", \"Canada\"] },\n  \"watch\": {\n    \"/form/country\": { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } }\n  },\n  \"children\": []\n}\n```\n\nWatchers only fire on value changes, not on initial render.\n\n## Validation\n\nBuilt-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`.\n\nCross-field validation uses `$state` expressions in args:\n\n```typescript\nimport { check } from \"@json-render/core\";\n\ncheck.required(\"Field is required\");\ncheck.matches(\"/form/password\", \"Passwords must match\");\ncheck.lessThan(\"/form/endDate\", \"Must be before end date\");\ncheck.greaterThan(\"/form/startDate\", \"Must be after start date\");\ncheck.requiredIf(\"/form/enableNotifications\", \"Required when enabled\");\n```\n\n## User Prompt Builder\n\nBuild structured user prompts with optional spec refinement and state context:\n\n```typescript\nimport { buildUserPrompt } from \"@json-render/core\";\n\n// Fresh generation\nbuildUserPrompt({ prompt: \"create a todo app\" });\n\n// Refinement with edit modes (default: patch-only)\nbuildUserPrompt({ prompt: \"add a toggle\", currentSpec: spec, editModes: [\"patch\", \"merge\"] });\n\n// With runtime state\nbuildUserPrompt({ prompt: \"show data\", state: { todos: [] } });\n```\n\nAvailable edit modes: `\"patch\"` (RFC 6902 JSON Patch), `\"merge\"` (RFC 7396 Merge Patch), `\"diff\"` (unified diff).\n\n## Spec Validation\n\nValidate spec structure and auto-fix common issues:\n\n```typescript\nimport { validateSpec, autoFixSpec } from \"@json-render/core\";\n\nconst { valid, issues } = validateSpec(spec);\n// issues include: missing_child, invalid_visible (malformed conditions),\n// repeat_without_children, repeat_item_outside_scope, repeat_state_mismatch\n\nconst { spec: fixed, fixDetails } = autoFixSpec(spec);\n// fixDetails entries are { message, lossy }. Lossless fixes relocate\n// misplaced fields; lossy fixes prune dangling children references.\n// In a repair loop, withhold lossy fixes until retries are exhausted:\nconst attempt = autoFixSpec(spec, { lossy: retriesExhausted });\n```\n\n## Visibility Conditions\n\nControl element visibility with state-based conditions. `VisibilityContext` is `{ stateModel: StateModel }`.\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\n// Syntax\n{ \"$state\": \"/path\" }                    // truthiness\n{ \"$state\": \"/path\", \"not\": true }      // falsy\n{ \"$state\": \"/path\", \"eq\": value }      // equality\n[ cond1, cond2 ]                         // implicit AND\n\n// Helpers\nvisibility.when(\"/path\")                 // { $state: \"/path\" }\nvisibility.unless(\"/path\")               // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)              // { $state: \"/path\", eq: val }\nvisibility.and(cond1, cond2)             // { $and: [cond1, cond2] }\nvisibility.or(cond1, cond2)              // { $or: [cond1, cond2] }\nvisibility.always                        // true\nvisibility.never                         // false\n```\n\n## Built-in Actions in Schema\n\nSchemas can declare `builtInActions` -- actions that are always available at runtime and auto-injected into prompts:\n\n```typescript\nconst schema = defineSchema(builder, {\n  builtInActions: [\n    { name: \"setState\", description: \"Update a value in the state model\" },\n  ],\n});\n```\n\nThese appear in prompts as `[built-in]` and don't require handlers in `defineRegistry`.\n\n## StateStore\n\nThe `StateStore` interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The `createStateStore` factory creates a simple in-memory implementation:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/core\";\n\nconst store = createStateStore({ count: 0 });\n\nstore.get(\"/count\");         // 0\nstore.set(\"/count\", 1);      // updates and notifies subscribers\nstore.update({ \"/a\": 1, \"/b\": 2 }); // batch update\n\nstore.subscribe(() => {\n  console.log(store.getSnapshot()); // { count: 1 }\n});\n```\n\nThe `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`, `getSnapshot()`, `subscribe(listener)`.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineSchema` | Create a new schema |\n| `defineCatalog` | Create a catalog from schema |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `resolvePropValue` | Resolve a single prop expression against data |\n| `resolveElementProps` | Resolve all prop expressions in an element |\n| `buildUserPrompt` | Build user prompts with refinement and state context |\n| `buildEditUserPrompt` | Build user prompt for editing existing specs |\n| `buildEditInstructions` | Generate prompt section for available edit modes |\n| `isNonEmptySpec` | Check if spec has root and at least one element |\n| `deepMergeSpec` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |\n| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |\n| `EditMode` | Type: `\"patch\" \\| \"merge\" \\| \"diff\"` |\n| `validateSpec` | Validate spec structure |\n| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |\n| `createSpecStreamCompiler` | Stream JSONL patches into spec |\n| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |\n| `parseSpecStreamLine` | Parse single JSONL line |\n| `applySpecStreamPatch` | Apply patch to object |\n| `StateStore` | Interface for plugging in external state management |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `check` | TypeScript helpers for creating validation checks |\n| `BuiltInAction` | Type for built-in action definitions (`name` + `description`) |\n| `ActionBinding` | Action binding type (includes `preventDefault` field) |\n","skills/devtools-react/SKILL.md":"---\nname: devtools-react\ndescription: React adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-react in a React json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-react\n\nReact adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-react\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing React json-render provider so it can observe the renderer's state, actions, and streams:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut.\n\n## Imperative controls\n\nUse `useJsonRenderDevtools` from any mounted React descendant:\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nThe hook returns `null` in production or before the component has mounted. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-solid/SKILL.md":"---\nname: devtools-solid\ndescription: SolidJS adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-solid in a SolidJS json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-solid\n\nSolidJS adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing SolidJS json-render provider:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-svelte/SKILL.md":"---\nname: devtools-svelte\ndescription: Svelte 5 adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-svelte in a Svelte json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-svelte\n\nSvelte 5 adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-svelte\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Svelte json-render provider:\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-vue/SKILL.md":"---\nname: devtools-vue\ndescription: Vue adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-vue in a Vue json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-vue\n\nVue adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-vue\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Vue json-render provider:\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools/SKILL.md":"---\nname: devtools\ndescription: Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include \"add devtools\", \"debug json-render\", \"inspect the spec\", \"why is this element not rendering\", \"see the state at runtime\", or requests to tap streams / capture action logs for `@json-render/devtools`.\n---\n\n# @json-render/devtools\n\nA floating inspector panel for json-render apps. Framework-agnostic core + per-framework adapters (React, Vue, Svelte, Solid).\n\nProduction-safe: the component renders no devtools UI when `process.env.NODE_ENV === \"production\"`, including when a bundler replaces the expression in a browser without a `process` global. Unbundled environments without `process.env` retain development behavior.\n\n## Install\n\nInstall the core package plus the adapter that matches the host app's renderer.\n\n```bash\n# React\nnpm install @json-render/devtools @json-render/devtools-react\n\n# Vue\nnpm install @json-render/devtools @json-render/devtools-vue\n\n# Svelte\nnpm install @json-render/devtools @json-render/devtools-svelte\n\n# Solid\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Drop-in usage\n\nPlace `<JsonRenderDevtools />` anywhere inside the existing `<JSONUIProvider>` (or framework equivalent). No other wiring required.\n\n### React\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\n### Vue\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\n### Svelte\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\n### Solid\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\n## Controls\n\n- Floating toggle appears bottom-right.\n- Hotkey: `Ctrl`/`Cmd` + `Shift` + `J` (configurable via `hotkey` prop).\n- Drawer is resizable; height persists to localStorage.\n\n## Props\n\n- `spec` (`Spec | null`) — current spec.\n- `catalog` (`Catalog | null`) — catalog definition; required for the Catalog panel.\n- `messages` (`UIMessage[]`) — AI SDK `useChat` messages; scanned for spec data parts.\n- `initialOpen` (`boolean`) — start open.\n- `position` (`\"bottom-right\" | \"bottom-left\" | \"right\"`) — dock + toggle corner. `\"bottom-*\"` docks at the bottom; `\"right\"` docks at the right edge full-height (recommended for app-shells that already use `100vh` or fixed bottom bars).\n- `hotkey` (`string | false`) — `\"mod+shift+j\"` by default.\n- `bufferSize` (`number`) — event ring-buffer cap, default 500.\n- `reserveSpace` (`boolean`, default `true`) — when true the panel pushes the host app by applying `padding-bottom` / `padding-right` on `body`. Set to `false` to keep the panel as a pure overlay.\n- `allowDockToggle` (`boolean`, default `true`) — show a toolbar button so the user can flip the panel between bottom-dock and right-dock. User choice persists to `localStorage` and overrides `position` on subsequent mounts. Pass `false` to lock the dock to `position`.\n- `onEvent` (`(DevtoolsEvent) => void`) — optional tap.\n\n## Panels\n\n- **Spec** — element tree rooted at `spec.root`; props/visibility/events/watchers detail; integrated `validateSpec` warnings.\n- **State** — every JSON Pointer path with inline edit via `store.set`.\n- **Actions** — dispatched actions timeline (name, params, result/error, duration).\n- **Stream** — spec patches, text chunks, token usage, lifecycle markers grouped by generation.\n- **Catalog** — components + actions declared in the catalog with prop chips.\n\n## Picker (toolbar)\n\nThe element picker is a toolbar button in the panel header (Chrome-DevTools-style), not a tab. Click it to activate pick mode, then click any rendered element in the page — selection jumps to the Spec tab with that element focused. `Esc` cancels.\n\n## Reserved space & docking\n\nThe panel can dock at the bottom or the right edge, and by default the user can flip between the two with a toolbar button (the choice persists to `localStorage`). Set `allowDockToggle={false}` if the host app only works with one dock — the button is hidden and the dock is locked to `position`.\n\nPick an initial dock that fits your layout:\n\n- **Bottom dock (default)** — works best for docs / marketing / content-flow sites and for app shells built with a `height: 100%` chain (`html { height: 100% }` → `body { height: 100% }` → `.app { height: 100% }`). The panel writes its height to `--jr-devtools-offset-bottom` and applies matching `padding-bottom` to `body`, so non-fixed content naturally makes room.\n- **Right dock** (`position=\"right\"`) — recommended for app-shell layouts that use `100vh` or `position: fixed; bottom: 0`. Right docking sidesteps the bottom edge entirely and writes its width to `--jr-devtools-offset-right` instead.\n\nApps that use `100vh`, `position: fixed`, or `position: sticky` can opt specific elements in with the published CSS custom properties:\n\n```css\n.composer   { bottom: var(--jr-devtools-offset-bottom, 0); }\n.sidebar    { right:  var(--jr-devtools-offset-right,  0); }\n.app-shell  { height: calc(100vh - var(--jr-devtools-offset-bottom, 0)); }\n```\n\nIf the automatic body padding causes problems with a particular layout, pass `reserveSpace={false}` to make the panel a pure overlay — the CSS custom properties are still published so you can reserve space manually.\n\n(`--jr-devtools-offset` is kept as a back-compat alias for whichever edge is currently active.)\n\n## Multiple renderers on one page (e.g. a chat)\n\nA single `<JsonRenderDevtools />` can inspect many `<Renderer />` instances at once — a chat where each assistant message renders its own spec, a dashboard made of several independent widgets, etc. The recipe:\n\n1. **One top-level `<JSONUIProvider>`** so every renderer shares one state store and one action dispatcher. Devtools lives inside this provider and sees everything through it.\n2. **Per-renderer specs, shared state** — each assistant message renders `<Renderer spec={msgSpec} registry={registry} />` directly, not wrapped in its own `StateProvider`. State paths from different messages must not collide.\n3. **Namespace state per turn** — when the source is an AI stream, hand the agent a unique `messageId` and require every element key (`<id>-root`) and state path (`/<id>/count`) to be prefixed with it.\n4. **Pass `spec={latest}` + `messages={all}`** — `spec` drives the Spec panel (usually the newest assistant message's spec), while `messages` feeds the Stream panel with patches from every turn.\n5. **Actions and the picker are already global** — `registerActionObserver` captures dispatches from any `ActionProvider` in the tree, and `data-jr-key` is written by the renderer itself, so Pick works across every rendered element regardless of which message produced it.\n\nSee `examples/devtools` for a full AI chat wired this way.\n\n## Imperative API (React only)\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nReturns `null` in production or before the component mounts.\n\n## Server-side stream tap\n\nCapture spec patches at the API route so events persist server-side or flow into your own telemetry.\n\n```ts\nimport { tapJsonRenderStream, createEventStore } from \"@json-render/devtools\";\nimport { pipeJsonRender } from \"@json-render/core\";\n\nconst events = createEventStore({ bufferSize: 1000 });\nconst tapped = tapJsonRenderStream(result.toUIMessageStream(), events);\nwriter.merge(pipeJsonRender(tapped));\n```\n\nYAML equivalent: `tapYamlStream`.\n\n## Under the hood\n\n- **Shadow-DOM isolated panel** — the panel's styles never leak into the host app and vice versa.\n- **Ring-buffered event store** — capped log of devtools events (state changes, action dispatches, stream patches, etc.).\n- **Action observer registry** — each framework's `ActionProvider` reports via `notifyActionDispatch` / `notifyActionSettle` in `@json-render/core`; devtools subscribes via `registerActionObserver`.\n- **Picker element tagging** — while devtools is mounted, `ElementRenderer` wraps each rendered element in `<span data-jr-key=\"...\" style=\"display:contents\">` so the picker can map DOM → spec key. No layout impact.\n","skills/directives/SKILL.md":"---\nname: directives\ndescription: Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs.\n---\n\n# @json-render/directives\n\nPre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.\n\n## Quick Start\n\n```typescript\nimport { standardDirectives } from '@json-render/directives';\n\n// Wire into prompt generation\nconst prompt = catalog.prompt({ directives: standardDirectives });\n\n// Wire into the renderer (React example)\nimport { JSONUIProvider, Renderer } from '@json-render/react';\n\n<JSONUIProvider registry={registry} directives={standardDirectives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\nTo add factory directives like `createI18nDirective`, spread the array:\n\n```typescript\nimport { standardDirectives, createI18nDirective } from '@json-render/directives';\n\nconst directives = [...standardDirectives, createI18nDirective(config)];\n```\n\n## Defining Custom Directives\n\nUse `defineDirective` from `@json-render/core`:\n\n```typescript\nimport { defineDirective, resolvePropValue } from '@json-render/core';\nimport { z } from 'zod';\n\nconst doubleDirective = defineDirective({\n  name: '$double',\n  description: 'Double a numeric value.',\n  schema: z.object({\n    $double: z.unknown(),\n  }),\n  resolve(value, ctx) {\n    const resolved = resolvePropValue(value.$double, ctx);\n    return (resolved as number) * 2;\n  },\n});\n```\n\nRules:\n- Name must start with `$`\n- Name must not conflict with built-in keys (`$state`, `$cond`, `$computed`, `$template`, `$item`, `$index`, `$bindState`, `$bindItem`)\n- Resolvers should call `resolvePropValue` on sub-values to support composition\n\n## Built-in Directives\n\n### `$format` — Locale-aware value formatting\n\nFormats values using `Intl` formatters. Supports `date`, `currency`, `number`, and `percent`.\n\n```json\n{ \"$format\": \"currency\", \"value\": { \"$state\": \"/cart/total\" }, \"currency\": \"USD\" }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/user/createdAt\" } }\n{ \"$format\": \"number\", \"value\": 1234567, \"notation\": \"compact\" }\n{ \"$format\": \"percent\", \"value\": 0.75 }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/post/createdAt\" }, \"style\": \"relative\" }\n```\n\nFields: `$format` (date | currency | number | percent), `value` (any expression), `locale?` (string), `currency?` (string, default \"USD\"), `notation?` (string), `style?` (\"relative\" for relative dates), `options?` (extra Intl options).\n\n### `$math` — Arithmetic operations\n\n```json\n{ \"$math\": \"add\", \"a\": { \"$state\": \"/subtotal\" }, \"b\": { \"$state\": \"/tax\" } }\n{ \"$math\": \"round\", \"a\": 3.7 }\n```\n\nOperations: `add`, `subtract`, `multiply`, `divide`, `mod`, `min`, `max`, `round`, `floor`, `ceil`, `abs`. Unary ops (`round`, `floor`, `ceil`, `abs`) only use `a`. Division by zero returns `0`.\n\nFields: `$math` (operation enum), `a?` (first operand, default 0), `b?` (second operand, default 0).\n\n### `$concat` — String concatenation\n\n```json\n{ \"$concat\": [{ \"$state\": \"/user/firstName\" }, \" \", { \"$state\": \"/user/lastName\" }] }\n```\n\nFields: `$concat` (array of values to resolve and join into a string).\n\n### `$count` — Array/string length\n\n```json\n{ \"$count\": { \"$state\": \"/cart/items\" } }\n```\n\nReturns `.length` of arrays or strings, `0` for other types.\n\nFields: `$count` (value to count).\n\n### `$truncate` — Text truncation\n\n```json\n{ \"$truncate\": { \"$state\": \"/post/body\" }, \"length\": 140, \"suffix\": \"...\" }\n```\n\nFields: `$truncate` (value to truncate), `length?` (max chars, default 100), `suffix?` (string, default \"...\").\n\n### `$pluralize` — Singular/plural forms\n\n```json\n{ \"$pluralize\": { \"$state\": \"/cart/itemCount\" }, \"one\": \"item\", \"other\": \"items\", \"zero\": \"no items\" }\n```\n\nOutput: `\"3 items\"`, `\"1 item\"`, or `\"no items\"`.\n\nFields: `$pluralize` (count value), `one` (singular label), `other` (plural label), `zero?` (zero label).\n\n### `$join` — Join array elements\n\n```json\n{ \"$join\": { \"$state\": \"/tags\" }, \"separator\": \", \" }\n```\n\nFields: `$join` (array to join), `separator?` (string, default \", \").\n\n### `createI18nDirective` — Internationalization factory\n\n```typescript\nimport { createI18nDirective } from '@json-render/directives';\n\nconst tDirective = createI18nDirective({\n  locale: 'en',\n  messages: {\n    en: { \"greeting\": \"Hello, {{name}}!\", \"checkout.submit\": \"Place Order\" },\n    es: { \"greeting\": \"Hola, {{name}}!\", \"checkout.submit\": \"Realizar Pedido\" },\n  },\n  fallbackLocale: 'en',\n});\n```\n\nUsage in specs:\n\n```json\n{ \"$t\": \"checkout.submit\" }\n{ \"$t\": \"greeting\", \"params\": { \"name\": { \"$state\": \"/user/name\" } } }\n```\n\nFields: `$t` (translation key), `params?` (interpolation parameters, values accept expressions).\n\nConfig: `locale` (current locale), `messages` (Record<locale, Record<key, string>>), `fallbackLocale?` (fallback when key missing).\n\n## Composition\n\nDirectives compose naturally — each resolver calls `resolvePropValue` on its inputs, so directives can wrap other directives or built-in expressions:\n\n```json\n{\n  \"$format\": \"currency\",\n  \"value\": { \"$math\": \"multiply\", \"a\": { \"$state\": \"/price\" }, \"b\": { \"$state\": \"/qty\" } },\n  \"currency\": \"USD\"\n}\n```\n\nResolves inside-out: `$state` reads from state, `$math` multiplies, `$format` formats as currency.\n\n## Wiring into Renderers\n\nAll four renderers (React, Vue, Svelte, Solid) accept `directives` on their provider and `createRenderer` output:\n\n```tsx\n// Provider pattern\n<JSONUIProvider registry={registry} directives={directives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n\n// createRenderer pattern\nconst MyRenderer = createRenderer(catalog, components);\n<MyRenderer spec={spec} directives={directives} />\n```\n\nFor prompt generation, pass the same array:\n\n```typescript\nconst prompt = catalog.prompt({ directives });\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `formatDirective` | `$format` directive definition |\n| `mathDirective` | `$math` directive definition |\n| `concatDirective` | `$concat` directive definition |\n| `countDirective` | `$count` directive definition |\n| `truncateDirective` | `$truncate` directive definition |\n| `pluralizeDirective` | `$pluralize` directive definition |\n| `joinDirective` | `$join` directive definition |\n| `createI18nDirective` | Factory for `$t` i18n directive |\n| `standardDirectives` | Array of all 7 non-factory directives |\n| `I18nConfig` | Type for i18n configuration |\n","skills/image/SKILL.md":"---\nname: image\ndescription: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.\n---\n\n# @json-render/image\n\nImage renderer that converts JSON specs into SVG and PNG images using Satori.\n\n## Quick Start\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"frame\",\n  elements: {\n    frame: {\n      type: \"Frame\",\n      props: { width: 1200, height: 630, backgroundColor: \"#1a1a2e\" },\n      children: [\"heading\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Hello World\", level: \"h1\", color: \"#ffffff\" },\n      children: [],\n    },\n  },\n};\n\nconst png = await renderToPng(spec, {\n  fonts: [{ name: \"Inter\", data: fontData, weight: 400, style: \"normal\" }],\n});\n```\n\n## Using Standard Components\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, standardComponentDefinitions } from \"@json-render/image\";\n\nexport const imageCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Adding Custom Components\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n});\n```\n\n## Standard Components\n\n| Component | Category | Description |\n|-----------|----------|-------------|\n| `Frame` | Root | Root container. Defines width, height, background. Must be root. |\n| `Box` | Layout | Container with padding, margin, border, absolute positioning |\n| `Row` | Layout | Horizontal flex layout |\n| `Column` | Layout | Vertical flex layout |\n| `Heading` | Content | h1-h4 heading text |\n| `Text` | Content | Body text with full styling |\n| `Image` | Content | Image from URL |\n| `Divider` | Decorative | Horizontal line separator |\n| `Spacer` | Decorative | Empty vertical space |\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `renderToSvg` | Render spec to SVG string |\n| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) |\n| `schema` | Image element schema |\n| `standardComponents` | Pre-built component registry |\n| `standardComponentDefinitions` | Catalog definitions for AI prompts |\n\n## Sub-path Exports\n\n| Export | Description |\n|--------|-------------|\n| `@json-render/image` | Full package: schema, components, render functions |\n| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) |\n| `@json-render/image/catalog` | Standard component definitions and types |\n| `@json-render/image/render` | Render functions only |\n","skills/ink/SKILL.md":"---\nname: ink\ndescription: Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.\n---\n\n# @json-render/ink\n\nInk terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/ink/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/ink/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/ink\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    CustomWidget: {\n      props: z.object({ title: z.string() }),\n      slots: [],\n      description: \"Custom widget\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    CustomWidget: ({ props }) => <Text>{props.title}</Text>,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <JSONUIProvider initialState={{}}>\n      <Renderer spec={spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Spec Structure (Flat Element Map)\n\nThe Ink schema uses a flat element map with a root key:\n\n```json\n{\n  \"root\": \"main\",\n  \"elements\": {\n    \"main\": {\n      \"type\": \"Box\",\n      \"props\": { \"flexDirection\": \"column\", \"padding\": 1 },\n      \"children\": [\"heading\", \"content\"]\n    },\n    \"heading\": {\n      \"type\": \"Heading\",\n      \"props\": { \"text\": \"Dashboard\", \"level\": \"h1\" },\n      \"children\": []\n    },\n    \"content\": {\n      \"type\": \"Text\",\n      \"props\": { \"text\": \"Hello from the terminal!\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Standard Components\n\n### Layout\n- `Box` - Flexbox layout container (like a terminal `<div>`). Use for grouping, spacing, borders, alignment. Default flexDirection is row.\n- `Text` - Text output with optional styling (color, bold, italic, etc.)\n- `Newline` - Inserts blank lines. Must be inside a Box with flexDirection column.\n- `Spacer` - Flexible empty space that expands along the main axis.\n\n### Content\n- `Heading` - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)\n- `Divider` - Horizontal separator with optional centered title\n- `Badge` - Colored inline label (variants: default, info, success, warning, error)\n- `Spinner` - Animated loading spinner with optional label\n- `ProgressBar` - Horizontal progress bar (0-1)\n- `Sparkline` - Inline chart using Unicode block characters\n- `BarChart` - Horizontal bar chart with labels and values\n- `Table` - Tabular data with headers and rows\n- `List` - Bulleted or numbered list\n- `ListItem` - Structured list row with title, subtitle, leading/trailing text\n- `Card` - Bordered container with optional title\n- `KeyValue` - Key-value pair display\n- `Link` - Clickable URL with optional label\n- `StatusLine` - Status message with colored icon (info, success, warning, error)\n- `Markdown` - Renders markdown text with terminal styling\n\n### Interactive\n- `TextInput` - Text input field (events: submit, change)\n- `Select` - Selection menu with arrow key navigation (events: change)\n- `MultiSelect` - Multi-selection with space to toggle (events: change, submit)\n- `ConfirmInput` - Yes/No confirmation prompt (events: confirm, deny)\n- `Tabs` - Tab bar navigation with left/right arrow keys (events: change)\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop of form components\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Event System\n\nComponents use `emit` to fire named events. The element's `on` field maps events to action bindings:\n\n```tsx\nCustomButton: ({ props, emit }) => (\n  <Box>\n    <Text>{props.label}</Text>\n    {/* emit(\"press\") triggers the action bound in the spec's on.press */}\n  </Box>\n),\n```\n\n```json\n{\n  \"type\": \"CustomButton\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\n`setState`, `pushState`, and `removeState` are built-in and handled automatically:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n```\n\n## Repeat (Dynamic Lists)\n\nUse the `repeat` field on a container element to render items from a state array:\n\n```json\n{\n  \"type\": \"Box\",\n  \"props\": { \"flexDirection\": \"column\" },\n  \"repeat\": { \"statePath\": \"/items\", \"key\": \"id\" },\n  \"children\": [\"item-row\"]\n}\n```\n\nInside repeated children, use `{ \"$item\": \"field\" }` to read from the current item and `{ \"$index\": true }` for the current index.\n\nFor nested lists, an inner repeat can use `{ \"statePath\": { \"$item\": \"children\" } }` to iterate an array on the enclosing item.\n\n## Streaming\n\nUse `useUIStream` to progressively render specs from JSONL patch streams:\n\n```tsx\nimport { useUIStream } from \"@json-render/ink\";\n\nconst { spec, send, isStreaming } = useUIStream({ api: \"/api/generate\" });\n```\n\n## Server-Side Prompt Generation\n\nUse the `./server` export to generate AI system prompts from your catalog:\n\n```typescript\nimport { catalog } from \"./catalog\";\n\nconst systemPrompt = catalog.prompt({ system: \"You are a terminal assistant.\" });\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched via the event system |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n| `FocusProvider` | Manage focus across interactive components |\n| `JSONUIProvider` | Combined provider for all contexts |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/ink\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## createRenderer (Higher-Level API)\n\n```tsx\nimport { createRenderer } from \"@json-render/ink\";\nimport { standardComponents } from \"@json-render/ink\";\nimport { catalog } from \"./catalog\";\n\nconst InkRenderer = createRenderer(catalog, {\n  ...standardComponents,\n  // custom component overrides here\n});\n\n// InkRenderer includes all providers (state, visibility, actions, focus)\nrender(\n  <InkRenderer spec={spec} state={{ activeTab: \"overview\" }} />\n);\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `createRenderer` | Higher-level: creates a component with built-in providers |\n| `JSONUIProvider` | Combined provider for all contexts |\n| `schema` | Ink flat element map schema (includes built-in state actions) |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way binding for `$bindState`/`$bindItem` expressions |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useOptionalValidation` | Non-throwing variant of useValidation |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n| `Components` | Typed component map (catalog-aware) |\n| `Actions` | Typed action map (catalog-aware) |\n| `ComponentContext` | Typed component context (catalog-aware) |\n| `flatToTree` | Convert flat element map to tree structure |\n\n## Terminal UI Design Guidelines\n\n- Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.\n- Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.\n- Use borderStyle on Box for visual grouping (single, double, round, bold).\n- Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.\n- Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.\n- Use Tabs for multi-view UIs with visible conditions on child content.\n- Use Sparkline for inline trends and BarChart for comparing values.\n","skills/jotai/SKILL.md":"---\nname: jotai\ndescription: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.\n---\n\n# @json-render/jotai\n\nJotai adapter for json-render's `StateStore` interface. Wire a Jotai atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/jotai @json-render/core @json-render/react jotai\n```\n\n## Usage\n\n```tsx\nimport { atom } from \"jotai\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom that holds the json-render state\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = jotaiStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Jotai */}\n</StateProvider>\n```\n\n### With a Shared Jotai Store\n\nWhen your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:\n\n```tsx\nimport { atom, createStore } from \"jotai\";\nimport { Provider as JotaiProvider } from \"jotai/react\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst jStore = createStore();\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\nconst store = jotaiStateStore({ atom: uiAtom, store: jStore });\n\n<JotaiProvider store={jStore}>\n  <StateProvider store={store}>\n    {/* Both json-render and useAtom() see the same state */}\n  </StateProvider>\n</JotaiProvider>\n```\n\n## API\n\n### `jotaiStateStore(options)`\n\nCreates a `StateStore` backed by a Jotai atom.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `WritableAtom<StateModel, [StateModel], void>` | Yes | A writable atom holding the state model |\n| `store` | Jotai `Store` | No | The Jotai store instance. Defaults to a new store. Pass your own to share state with `<Provider>`. |\n","skills/mcp/SKILL.md":"---\nname: mcp\ndescription: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.\n---\n\n# @json-render/mcp\n\nMCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Quick Start\n\n### Server (Node.js)\n\n```typescript\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...shadcnComponentDefinitions },\n  actions: {},\n});\n\nconst server = createMcpApp({\n  name: \"My App\",\n  version: \"1.0.0\",\n  catalog,\n  html: fs.readFileSync(\"dist/index.html\", \"utf-8\"),\n});\n\nawait server.connect(new StdioServerTransport());\n```\n\n### Client (React, inside iframe)\n\n```tsx\nimport { useJsonRenderApp } from \"@json-render/mcp/app\";\nimport { JSONUIProvider, Renderer } from \"@json-render/react\";\n\nfunction McpAppView({ registry }) {\n  const { spec, loading, error } = useJsonRenderApp();\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting...</div>;\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Architecture\n\n1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource\n2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs\n3. The HTML resource is a Vite-bundled single-file React app with json-render renderers\n4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs\n\n## Server API\n\n- `createMcpApp(options)` - main entry, creates a full MCP server\n- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server\n- `registerJsonRenderResource(server, options)` - register the UI resource\n\n## Client API (`@json-render/mcp/app`)\n\n- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`\n- `buildAppHtml(options)` - generate HTML from bundled JS/CSS\n\n## Building the iframe HTML\n\nBundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\n\nexport default defineConfig({\n  plugins: [react(), viteSingleFile()],\n  build: { outDir: \"dist\" },\n});\n```\n\n## Client Configuration\n\n### Cursor (`.cursor/mcp.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## Dependencies\n\n```bash\n# Server\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n\n# Client (iframe)\nnpm install @json-render/react @json-render/shadcn react react-dom\n\n# Build tools\nnpm install -D vite @vitejs/plugin-react vite-plugin-singlefile\n```\n","skills/next/SKILL.md":"---\nname: next\ndescription: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.\n---\n\n# @json-render/next\n\nNext.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.\n\n## Quick Start\n\n```bash\nnpm install @json-render/core @json-render/react @json-render/next\n```\n\n### 1. Define Your Spec\n\n```typescript\n// lib/spec.ts\nimport type { NextAppSpec } from \"@json-render/next\";\n\nexport const spec: NextAppSpec = {\n  metadata: {\n    title: { default: \"My App\", template: \"%s | My App\" },\n    description: \"A json-render Next.js application\",\n  },\n  layouts: {\n    main: {\n      root: \"shell\",\n      elements: {\n        shell: { type: \"Container\", props: {}, children: [\"nav\", \"slot\"] },\n        nav: { type: \"NavBar\", props: { links: [\n          { href: \"/\", label: \"Home\" },\n          { href: \"/about\", label: \"About\" },\n        ]}, children: [] },\n        slot: { type: \"Slot\", props: {}, children: [] },\n      },\n    },\n  },\n  routes: {\n    \"/\": {\n      layout: \"main\",\n      metadata: { title: \"Home\" },\n      page: {\n        root: \"hero\",\n        elements: {\n          hero: { type: \"Card\", props: { title: \"Welcome\" }, children: [] },\n        },\n      },\n    },\n    \"/about\": {\n      layout: \"main\",\n      metadata: { title: \"About\" },\n      page: {\n        root: \"content\",\n        elements: {\n          content: { type: \"Card\", props: { title: \"About Us\" }, children: [] },\n        },\n      },\n    },\n  },\n};\n```\n\n### 2. Create the App\n\n```typescript\n// lib/app.ts\nimport { createNextApp } from \"@json-render/next/server\";\nimport { spec } from \"./spec\";\n\nexport const { Page, generateMetadata, generateStaticParams } = createNextApp({\n  spec,\n  loaders: {\n    // Server-side data loaders (optional)\n    loadPost: async ({ slug }) => {\n      const post = await getPost(slug as string);\n      return { post };\n    },\n  },\n});\n```\n\n### 3. Wire Up Route Files\n\n```tsx\n// app/[[...slug]]/page.tsx\nexport { Page as default, generateMetadata, generateStaticParams } from \"@/lib/app\";\n```\n\n```tsx\n// app/[[...slug]]/layout.tsx\nimport { NextAppProvider } from \"@json-render/next\";\nimport { registry, handlers } from \"@/lib/registry\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <NextAppProvider registry={registry} handlers={handlers}>\n          {children}\n        </NextAppProvider>\n      </body>\n    </html>\n  );\n}\n```\n\n## Key Concepts\n\n### NextAppSpec\n\nThe top-level spec defines an entire Next.js application:\n\n- **metadata**: Root-level SEO metadata (title template, description, OpenGraph)\n- **layouts**: Reusable layout element trees (each must include a `Slot` component)\n- **routes**: Route definitions keyed by URL pattern\n- **state**: Global initial state shared across all routes\n\n### Route Patterns\n\nRoutes use Next.js URL conventions:\n\n- `\"/\"` -- home page\n- `\"/about\"` -- static route\n- `\"/blog/[slug]\"` -- dynamic segment\n- `\"/docs/[...path]\"` -- catch-all segment\n- `\"/settings/[[...path]]\"` -- optional catch-all segment\n\n### Layouts\n\nLayouts wrap page content. Every layout MUST include a `Slot` component where page content will be rendered. Layouts are defined once in `spec.layouts` and referenced by routes via the `layout` field.\n\n### Built-in Components\n\n- **Slot**: Placeholder in layouts where page content is rendered\n- **Link**: Client-side navigation link (wraps `next/link`)\n\n### Built-in Actions\n\n- **setState**: Update state value. Params: `{ statePath, value }`\n- **pushState**: Append to array. Params: `{ statePath, value, clearStatePath? }`\n- **removeState**: Remove from array by index. Params: `{ statePath, index }`\n- **navigate**: Client-side navigation. Params: `{ href }`\n\n### Data Loaders\n\nServer-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.\n\n```typescript\ncreateNextApp({\n  spec,\n  loaders: {\n    loadPost: async ({ slug }) => {\n      const post = await db.post.findUnique({ where: { slug } });\n      return { post };\n    },\n  },\n});\n```\n\n### SSR\n\nPages are server-rendered automatically. The `createNextApp` `Page` component is an async Server Component that:\n\n1. Matches the route from the spec\n2. Runs server-side data loaders\n3. Generates metadata\n4. Passes the resolved spec to the client renderer for hydration\n\n### Entry Points\n\n- `@json-render/next` -- Client components (NextAppProvider, PageRenderer, Link)\n- `@json-render/next/server` -- Server utilities (createNextApp, matchRoute, schema)\n\n## API Reference\n\n### Server Exports (`@json-render/next/server`)\n\n- `createNextApp(options)` -- Create Page, generateMetadata, generateStaticParams\n- `schema` -- Custom schema for Next.js apps (for AI catalog generation)\n- `matchRoute(spec, pathname)` -- Match a URL to a route spec\n- `resolveMetadata(spec, route)` -- Resolve metadata for a route\n- `slugToPath(slug)` -- Convert catch-all slug array to pathname\n- `collectStaticParams(spec)` -- Collect static params for all routes\n\n### Client Exports (`@json-render/next`)\n\n- `NextAppProvider` -- Context provider for registry and handlers\n- `PageRenderer` -- Renders a page spec with optional layout\n- `NextErrorBoundary` -- Error boundary component\n- `NextLoading` -- Loading state component\n- `NextNotFound` -- Not-found component\n- `Link` -- Built-in navigation component (wraps next/link)\n","skills/react-email/SKILL.md":"---\nname: react-email\ndescription: React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email.\nmetadata:\n  tags: react-email, email, json-render, html email, transactional email\n---\n\n# @json-render/react-email\n\nReact Email renderer that converts JSON specs into HTML or plain-text email output.\n\n## Quick Start\n\n```typescript\nimport { renderToHtml } from \"@json-render/react-email\";\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email\";\nimport { defineCatalog } from \"@json-render/core\";\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n\nconst spec = {\n  root: \"html-1\",\n  elements: {\n    \"html-1\": { type: \"Html\", props: { lang: \"en\", dir: \"ltr\" }, children: [\"head-1\", \"body-1\"] },\n    \"head-1\": { type: \"Head\", props: {}, children: [] },\n    \"body-1\": {\n      type: \"Body\",\n      props: { style: { backgroundColor: \"#f6f9fc\" } },\n      children: [\"container-1\"],\n    },\n    \"container-1\": {\n      type: \"Container\",\n      props: { style: { maxWidth: \"600px\", margin: \"0 auto\", padding: \"20px\" } },\n      children: [\"heading-1\", \"text-1\"],\n    },\n    \"heading-1\": { type: \"Heading\", props: { text: \"Welcome\" }, children: [] },\n    \"text-1\": { type: \"Text\", props: { text: \"Thanks for signing up.\" }, children: [] },\n  },\n};\n\nconst html = await renderToHtml(spec);\n```\n\n## Spec Structure (Element Tree)\n\nSame flat element tree as `@json-render/react`: `root` key plus `elements` map. Root must be `Html`; children of `Html` should be `Head` and `Body`. Use `Container` (e.g. max-width 600px) inside `Body` for client-safe layout.\n\n## Creating a Catalog and Registry\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToHtml } from \"@json-render/react-email\";\nimport { standardComponentDefinitions } from \"@json-render/react-email/catalog\";\nimport { Container, Heading, Text } from \"@react-email/components\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Alert: {\n      props: z.object({\n        message: z.string(),\n        variant: z.enum([\"info\", \"success\", \"warning\"]).nullable(),\n      }),\n      slots: [],\n      description: \"A highlighted message block\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Alert: ({ props }) => (\n      <Container style={{ padding: 16, backgroundColor: \"#eff6ff\", borderRadius: 8 }}>\n        <Text style={{ margin: 0 }}>{props.message}</Text>\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\n## Server-Side Render APIs\n\n| Function | Purpose |\n|----------|---------|\n| `renderToHtml(spec, options?)` | Render spec to HTML email string |\n| `renderToPlainText(spec, options?)` | Render spec to plain-text email string |\n\n`RenderOptions`: `registry`, `includeStandard` (default true), `state` (for `$state` / `$cond`).\n\n## Visibility and State\n\nSupports `visible` conditions, `$state`, `$cond`, repeat (`repeat.statePath`), nested repeat paths with `{ \"$item\": \"field\" }`, and the same expression syntax as `@json-render/react`. Use `state` in `RenderOptions` when rendering server-side so expressions resolve.\n\n## Server-Safe Import\n\nImport schema and catalog without React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email/server\";\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create type-safe component registry from catalog |\n| `Renderer` | Render spec in browser (e.g. preview); use with `JSONUIProvider` for state/actions |\n| `createRenderer` | Standalone renderer component with state/actions/validation |\n| `renderToHtml` | Server: spec to HTML string |\n| `renderToPlainText` | Server: spec to plain-text string |\n| `schema` | Email element schema |\n| `standardComponents` | Pre-built component implementations |\n| `standardComponentDefinitions` | Catalog definitions (Zod props) |\n\n## Sub-path Exports\n\n| Path | Purpose |\n|------|---------|\n| `@json-render/react-email` | Full package |\n| `@json-render/react-email/server` | Schema and catalog only (no React) |\n| `@json-render/react-email/catalog` | Standard component definitions and types |\n| `@json-render/react-email/render` | Render functions only |\n\n## Standard Components\n\nAll components accept a `style` prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.\n\n### Document structure\n\n| Component | Description |\n|-----------|-------------|\n| `Html` | Root wrapper (lang, dir). Children: Head, Body. |\n| `Head` | Email head section. |\n| `Body` | Body wrapper; use `style` for background. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Constrain width (e.g. max-width 600px). |\n| `Section` | Group content; table-based for compatibility. |\n| `Row` | Horizontal row. |\n| `Column` | Column in a Row; set width via style. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (as: h1–h6). |\n| `Text` | Body text. |\n| `Link` | Hyperlink (text, href). |\n| `Button` | CTA link styled as button (text, href). |\n| `Image` | Image from URL (src, alt, width, height). |\n| `Hr` | Horizontal rule. |\n\n### Utility\n\n| Component | Description |\n|-----------|-------------|\n| `Preview` | Inbox preview text (inside Html). |\n| `Markdown` | Markdown content as email-safe HTML. |\n\n## Email Best Practices\n\n- Keep width constrained (e.g. Container max-width 600px).\n- Use inline styles or React Email's style props; many clients strip `<style>` blocks.\n- Prefer table-based layout (Section, Row, Column) for broad client support.\n- Use absolute URLs for images; many clients block relative or cid: references in some contexts.\n- Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.\n","skills/react-native/SKILL.md":"---\nname: react-native\ndescription: React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile.\n---\n\n# @json-render/react-native\n\nReact Native renderer that converts JSON specs into native mobile component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-native/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/react-native/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/react-native\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Icon: {\n      props: z.object({ name: z.string(), size: z.number().nullable(), color: z.string().nullable() }),\n      slots: [],\n      description: \"Icon display\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{}}>\n          <Renderer spec={spec} registry={registry} />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Standard Components\n\n### Layout\n- `Container` - wrapper with padding, background, border radius\n- `Row` - horizontal flex layout with gap, alignment\n- `Column` - vertical flex layout with gap, alignment\n- `ScrollContainer` - scrollable area (vertical or horizontal)\n- `SafeArea` - safe area insets for notch/home indicator\n- `Pressable` - touchable wrapper that triggers actions on press\n- `Spacer` - fixed or flexible spacing\n- `Divider` - thin line separator\n\n### Content\n- `Heading` - heading text (levels 1-6)\n- `Paragraph` - body text\n- `Label` - small label text\n- `Image` - image display with sizing modes\n- `Avatar` - circular avatar image\n- `Badge` - small status badge\n- `Chip` - tag/chip for categories\n\n### Input\n- `Button` - pressable button with variants\n- `TextInput` - text input field\n- `Switch` - toggle switch\n- `Checkbox` - checkbox with label\n- `Slider` - range slider\n- `SearchBar` - search input\n\n### Feedback\n- `Spinner` - loading indicator\n- `ProgressBar` - progress indicator\n\n### Composite\n- `Card` - card container with optional header\n- `ListItem` - list row with title, subtitle, accessory\n- `Modal` - bottom sheet modal\n\n## Visibility Conditions\n\nUse `visible` on elements. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `[ cond1, cond2 ]` for AND.\n\n## Pressable + setState Pattern\n\nUse `Pressable` with the built-in `setState` action for interactive UIs like tab bars:\n\n```json\n{\n  \"type\": \"Pressable\",\n  \"props\": {\n    \"action\": \"setState\",\n    \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n  },\n  \"children\": [\"home-icon\", \"home-label\"]\n}\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- Nested lists use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" } } }` inside an enclosing repeat.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n\n```json\n{\n  \"type\": \"TextInput\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Built-in Actions\n\nThe `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:\n\n```json\n{ \"action\": \"setState\", \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched from components |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `schema` | React Native element tree schema |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `createStandardActionHandlers` | Create handlers for standard actions |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way state binding via `$bindState`/`$bindItem` |\n| `useStateBinding` | _(deprecated)_ Legacy two-way binding by path |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n","skills/react-pdf/SKILL.md":"---\nname: react-pdf\ndescription: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files.\n---\n\n# @json-render/react-pdf\n\nReact PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\n## Quick Start\n\n```typescript\nimport { renderToBuffer } from \"@json-render/react-pdf\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"doc\",\n  elements: {\n    doc: { type: \"Document\", props: { title: \"Invoice\" }, children: [\"page\"] },\n    page: {\n      type: \"Page\",\n      props: { size: \"A4\" },\n      children: [\"heading\", \"table\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoice #1234\", level: \"h1\" },\n      children: [],\n    },\n    table: {\n      type: \"Table\",\n      props: {\n        columns: [\n          { header: \"Item\", width: \"60%\" },\n          { header: \"Price\", width: \"40%\", align: \"right\" },\n        ],\n        rows: [\n          [\"Widget A\", \"$10.00\"],\n          [\"Widget B\", \"$25.00\"],\n        ],\n      },\n      children: [],\n    },\n  },\n};\n\nconst buffer = await renderToBuffer(spec);\n```\n\n## Render APIs\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from \"@json-render/react-pdf\";\n\n// In-memory buffer\nconst buffer = await renderToBuffer(spec);\n\n// Readable stream (pipe to HTTP response)\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\n// Direct to file\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nAll render functions accept an optional second argument: `{ registry?, state?, handlers? }`.\n\n## Standard Components\n\n| Component | Description |\n|-----------|-------------|\n| `Document` | Top-level PDF wrapper (must be root) |\n| `Page` | Page with size (A4, LETTER), orientation, margins |\n| `View` | Generic container (padding, margin, background, border) |\n| `Row`, `Column` | Flex layout with gap, align, justify |\n| `Heading` | h1-h4 heading text |\n| `Text` | Body text (fontSize, color, weight, alignment) |\n| `Image` | Image from URL or base64 |\n| `Link` | Hyperlink with text and href |\n| `Table` | Data table with typed columns and rows |\n| `List` | Ordered or unordered list |\n| `Divider` | Horizontal line separator |\n| `Spacer` | Empty vertical space |\n| `PageNumber` | Current page number and total pages |\n\n## Custom Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToBuffer } from \"@json-render/react-pdf\";\nimport { standardComponentDefinitions } from \"@json-render/react-pdf/catalog\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Badge: ({ props }) => (\n      <View style={{ backgroundColor: props.color ?? \"#e5e7eb\", padding: 4 }}>\n        <Text>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## External Store (Controlled Mode)\n\nPass a `StateStore` for full control over state:\n\nNested lists can set `repeat.statePath` to `{ \"$item\": \"field\" }` inside an enclosing repeat.\n\n```typescript\nimport { createStateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\nstore.set(\"/invoice/total\", 200);\n```\n\n## Server-Safe Import\n\nImport schema and catalog without pulling in React:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-pdf/server\";\n```\n","skills/react-three-fiber/SKILL.md":"---\nname: react-three-fiber\ndescription: React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs.\n---\n\n# @json-render/react-three-fiber\n\nReact Three Fiber renderer for json-render. 19 built-in 3D components.\n\n## Two Entry Points\n\n| Entry Point | Exports | Use For |\n|-------------|---------|---------|\n| `@json-render/react-three-fiber/catalog` | `threeComponentDefinitions` | Catalog schemas (no R3F dependency, safe for server) |\n| `@json-render/react-three-fiber` | `threeComponents`, `ThreeRenderer`, `ThreeCanvas`, schemas | R3F implementations and renderer |\n\n## Usage Pattern\n\nPick the 3D components you need from the standard definitions:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { threeComponentDefinitions } from \"@json-render/react-three-fiber/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { threeComponents, ThreeCanvas } from \"@json-render/react-three-fiber\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Box: threeComponentDefinitions.Box,\n    Sphere: threeComponentDefinitions.Sphere,\n    AmbientLight: threeComponentDefinitions.AmbientLight,\n    DirectionalLight: threeComponentDefinitions.DirectionalLight,\n    OrbitControls: threeComponentDefinitions.OrbitControls,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Box: threeComponents.Box,\n    Sphere: threeComponents.Sphere,\n    AmbientLight: threeComponents.AmbientLight,\n    DirectionalLight: threeComponents.DirectionalLight,\n    OrbitControls: threeComponents.OrbitControls,\n  },\n});\n```\n\n## Rendering\n\n### ThreeCanvas (convenience wrapper)\n\n```tsx\n<ThreeCanvas\n  spec={spec}\n  registry={registry}\n  shadows\n  camera={{ position: [5, 5, 5], fov: 50 }}\n  style={{ width: \"100%\", height: \"100vh\" }}\n/>\n```\n\n### Manual Canvas setup\n\n```tsx\nimport { Canvas } from \"@react-three/fiber\";\nimport { ThreeRenderer } from \"@json-render/react-three-fiber\";\n\n<Canvas shadows>\n  <ThreeRenderer spec={spec} registry={registry}>\n    {/* Additional R3F elements */}\n  </ThreeRenderer>\n</Canvas>\n```\n\n## Available Components (19)\n\n### Primitives (7)\n- `Box` -- width, height, depth, material\n- `Sphere` -- radius, widthSegments, heightSegments, material\n- `Cylinder` -- radiusTop, radiusBottom, height, material\n- `Cone` -- radius, height, material\n- `Torus` -- radius, tube, material\n- `Plane` -- width, height, material\n- `Capsule` -- radius, length, material\n\nAll primitives share: `position`, `rotation`, `scale`, `castShadow`, `receiveShadow`, `material`.\n\n### Lights (4)\n- `AmbientLight` -- color, intensity\n- `DirectionalLight` -- position, color, intensity, castShadow\n- `PointLight` -- position, color, intensity, distance, decay\n- `SpotLight` -- position, color, intensity, angle, penumbra\n\n### Other (8)\n- `Group` -- container with position/rotation/scale, supports children\n- `Model` -- GLTF/GLB loader via url prop\n- `Environment` -- HDRI environment map (preset, background, blur, intensity)\n- `Fog` -- linear fog (color, near, far)\n- `GridHelper` -- reference grid (size, divisions, color)\n- `Text3D` -- SDF text (text, fontSize, color, anchorX, anchorY)\n- `PerspectiveCamera` -- camera (position, fov, near, far, makeDefault)\n- `OrbitControls` -- orbit controls (enableDamping, enableZoom, autoRotate)\n\n## Shared Schemas\n\nReusable Zod schemas for custom 3D catalog definitions:\n\n```typescript\nimport { vector3Schema, materialSchema, transformProps, shadowProps } from \"@json-render/react-three-fiber\";\nimport { z } from \"zod\";\n\n// Custom 3D component\nconst myComponentDef = {\n  props: z.object({\n    ...transformProps,\n    ...shadowProps,\n    material: materialSchema.nullable(),\n    myCustomProp: z.string(),\n  }),\n  description: \"My custom 3D component\",\n};\n```\n\n## Material Schema\n\n```typescript\nmaterialSchema = z.object({\n  color: z.string().nullable(),         // default \"#ffffff\"\n  metalness: z.number().nullable(),     // default 0\n  roughness: z.number().nullable(),     // default 1\n  emissive: z.string().nullable(),      // default \"#000000\"\n  emissiveIntensity: z.number().nullable(), // default 1\n  opacity: z.number().nullable(),       // default 1\n  transparent: z.boolean().nullable(),  // default false\n  wireframe: z.boolean().nullable(),    // default false\n});\n```\n\n## Spec Format\n\n3D specs use the standard json-render flat element format:\n\n```json\n{\n  \"root\": \"scene\",\n  \"elements\": {\n    \"scene\": {\n      \"type\": \"Group\",\n      \"props\": { \"position\": [0, 0, 0] },\n      \"children\": [\"light\", \"box\"]\n    },\n    \"light\": {\n      \"type\": \"AmbientLight\",\n      \"props\": { \"intensity\": 0.5 },\n      \"children\": []\n    },\n    \"box\": {\n      \"type\": \"Box\",\n      \"props\": {\n        \"position\": [0, 0.5, 0],\n        \"material\": { \"color\": \"#4488ff\", \"metalness\": 0.3, \"roughness\": 0.7 }\n      },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Dependencies\n\nPeer dependencies required:\n- `@react-three/fiber` >= 8.0.0\n- `@react-three/drei` >= 9.0.0\n- `three` >= 0.160.0\n- `react` ^19.0.0\n- `zod` ^4.0.0\n","skills/react/SKILL.md":"---\nname: react\ndescription: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.\n---\n\n# @json-render/react\n\nReact renderer that converts JSON specs into React component trees.\n\n## Quick Start\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => <div>{props.title}{children}</div>,\n  },\n});\n\nfunction App({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { z } from \"zod\";\n\n// Create catalog with props schemas\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n      description: \"Card container with title\",\n    },\n    Layout: {\n      props: z.object({}),\n      slots: [\"default\", \"header\", \"footer\"],\n      description: \"Layout with named content regions\",\n    },\n  },\n});\n\n// Define component implementations with type-safe props\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Button: ({ props }) => (\n      <button className={props.variant}>{props.label}</button>\n    ),\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h2>{props.title}</h2>\n        {children}\n      </div>\n    ),\n    Layout: ({ children, slots }) => (\n      <div>\n        <header>{slots?.header}</header>\n        <main>{children}</main>\n        <footer>{slots?.footer}</footer>\n      </div>\n    ),\n  },\n});\n```\n\n## Spec Structure (Element Tree)\n\nThe React schema uses an element tree format:\n\n```json\n{\n  \"root\": {\n    \"type\": \"Card\",\n    \"props\": { \"title\": \"Hello\" },\n    \"children\": [{ \"type\": \"Button\", \"props\": { \"label\": \"Click me\" } }]\n  }\n}\n```\n\n## Named Slots\n\nUse `children` for the `\"default\"` slot. Use the element's top-level `slots` object for other slot names declared by the catalog:\n\n```json\n{\n  \"type\": \"Layout\",\n  \"props\": {},\n  \"children\": [\"main\"],\n  \"slots\": {\n    \"header\": [\"heading\"],\n    \"footer\": [\"actions\"]\n  }\n}\n```\n\nRegistry components receive named content as `slots?.header`, `slots?.footer`, and so on. Do not use `slots.default`.\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. New syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR. Helpers: `visibility.when(\"/path\")`, `visibility.unless(\"/path\")`, `visibility.eq(\"/path\", val)`, `visibility.and(cond1, cond2)`, `visibility.or(cond1, cond2)`.\n\n## Providers\n\n| Provider             | Purpose                                                                                                |\n| -------------------- | ------------------------------------------------------------------------------------------------------ |\n| `StateProvider`      | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider`     | Handle actions dispatched via the event system                                                         |\n| `VisibilityProvider` | Enable conditional rendering based on state                                                            |\n| `ValidationProvider` | Form field validation                                                                                  |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>;\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved by the renderer before components receive props:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **Filtered lists**: `repeat` plus an `$item` visible condition on the same container renders only matching items: `{ \"repeat\": { \"statePath\": \"/tasks\", \"key\": \"id\" }, \"visible\": { \"$item\": \"status\", \"eq\": \"todo\" }, \"children\": [\"task-card\"] }`. AND-composed `$state` conjuncts gate the container shell; `$item`/`$index` conjuncts filter items.\n- **Nested lists**: inside a repeat, use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" }, \"key\": \"id\" } }` to iterate an array on the enclosing item.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n- **`{ \"$computed\": \"fn\", \"args\": { ... } }`** - calls registered functions with resolved args\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\nComponents receive already-resolved props. For two-way bound props, use the `useBoundProp` hook with the `bindings` map the renderer provides.\n\nRegister `$computed` functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:\n\n```tsx\n<JSONUIProvider\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n```\n\n## Event System\n\nComponents use `emit` to fire named events, or `on()` to get an event handle with metadata. The element's `on` field maps events to action bindings:\n\n```tsx\n// Simple event firing\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n),\n\n// Event handle with metadata (e.g. preventDefault)\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a href={props.href} onClick={(e) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    }}>{props.label}</a>\n  );\n},\n```\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } }\n}\n```\n\nThe `EventHandle` returned by `on()` has: `emit()`, `shouldPreventDefault` (boolean), and `bound` (boolean).\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\"]\n  },\n  \"watch\": { \"/form/country\": { \"action\": \"loadCities\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\nThe `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in catalog `actions`:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n{ \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } }\n```\n\n`validateForm` validates all registered fields and writes `{ valid, errors }` to state.\n\nNote: `statePath` in action params (e.g. `setState.statePath`) targets the mutation path. Two-way binding in component props uses `{ \"$bindState\": \"/path\" }` on the value prop, not `statePath`.\n\n## useBoundProp\n\nFor form components that need two-way binding, use `useBoundProp` with the `bindings` map the renderer provides when a prop uses `{ \"$bindState\": \"/path\" }` or `{ \"$bindItem\": \"field\" }`:\n\n```tsx\nimport { useBoundProp } from \"@json-render/react\";\n\nInput: ({ element, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(\n    element.props.value,\n    bindings?.value\n  );\n  return (\n    <input\n      value={value ?? \"\"}\n      onChange={(e) => setValue(e.target.value)}\n    />\n  );\n},\n```\n\n`useBoundProp(propValue, bindingPath)` returns `[value, setValue]`. The `value` is the resolved prop; `setValue` writes back to the bound state path (no-op if not bound).\n\n## BaseComponentProps\n\nFor building reusable component libraries not tied to a specific catalog (e.g. `@json-render/shadcn`):\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/react\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (\n  <div>{props.title}{children}</div>\n);\n```\n\n## defineRegistry\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it.\n\n## Key Exports\n\n| Export                  | Purpose                                                                                               |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `defineRegistry`        | Create a type-safe component registry from a catalog                                                  |\n| `Renderer`              | Render a spec using a registry                                                                        |\n| `schema`                | Element tree schema (includes built-in state actions: setState, pushState, removeState, validateForm) |\n| `useStateStore`         | Access state context                                                                                  |\n| `useStateValue`         | Get single value from state                                                                           |\n| `useBoundProp`          | Two-way binding for `$bindState`/`$bindItem` expressions                                              |\n| `useActions`            | Access actions context                                                                                |\n| `useAction`             | Get a single action dispatch function                                                                 |\n| `useOptionalValidation` | Non-throwing variant of useValidation (returns null if no provider)                                   |\n| `useUIStream`           | Stream specs from an API endpoint                                                                     |\n| `createStateStore`      | Create a framework-agnostic in-memory `StateStore`                                                    |\n| `StateStore`            | Interface for plugging in external state management                                                   |\n| `BaseComponentProps`    | Catalog-agnostic base type for reusable component libraries                                           |\n| `EventHandle`           | Event handle type (`emit`, `shouldPreventDefault`, `bound`)                                           |\n| `ComponentContext`      | Typed component context (catalog-aware)                                                               |\n","skills/redux/SKILL.md":"---\nname: redux\ndescription: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux.\n---\n\n# @json-render/redux\n\nRedux adapter for json-render's `StateStore` interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/redux @json-render/core @json-render/react redux\n# or with Redux Toolkit (recommended):\nnpm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit\n```\n\n## Usage\n\n```tsx\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Define a slice for json-render state\nconst uiSlice = createSlice({\n  name: \"ui\",\n  initialState: { count: 0 } as Record<string, unknown>,\n  reducers: {\n    replaceUiState: (_state, action) => action.payload,\n  },\n});\n\n// 2. Create the Redux store\nconst reduxStore = configureStore({\n  reducer: { ui: uiSlice.reducer },\n});\n\n// 3. Create the json-render StateStore adapter\nconst store = reduxStateStore({\n  store: reduxStore,\n  selector: (state) => state.ui,\n  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n});\n\n// 4. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Redux */}\n</StateProvider>\n```\n\n## API\n\n### `reduxStateStore(options)`\n\nCreates a `StateStore` backed by a Redux store.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `Store` | Yes | The Redux store instance |\n| `selector` | `(state) => StateModel` | Yes | Select the json-render slice from the Redux state tree. Use `(s) => s` if the entire state is the model. |\n| `dispatch` | `(nextState, store) => void` | Yes | Dispatch an action that replaces the selected slice with the next state |\n\nThe `dispatch` callback receives the full next state model and the Redux store.\n","skills/remotion-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2FSKILL.md -->","skills/remotion-best-practices/rules/3d.md":"---\nname: 3d\ndescription: 3D content in Remotion using Three.js and React Three Fiber.\nmetadata:\n  tags: 3d, three, threejs\n---\n\n# Using Three.js and React Three Fiber in Remotion\n\nFollow React Three Fiber and Three.js best practices.  \nOnly the following Remotion-specific rules need to be followed:\n\n## Prerequisites\n\nFirst, the `@remotion/three` package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/three # If project uses npm\nbunx remotion add @remotion/three # If project uses bun\nyarn remotion add @remotion/three # If project uses yarn\npnpm exec remotion add @remotion/three # If project uses pnpm\n```\n\n## Using ThreeCanvas\n\nYou MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.  \n`<ThreeCanvas>` MUST have a `width` and `height` prop.\n\n```tsx\nimport { ThreeCanvas } from \"@remotion/three\";\nimport { useVideoConfig } from \"remotion\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <ambientLight intensity={0.4} />\n  <directionalLight position={[5, 5, 5]} intensity={0.8} />\n  <mesh>\n    <sphereGeometry args={[1, 32, 32]} />\n    <meshStandardMaterial color=\"red\" />\n  </mesh>\n</ThreeCanvas>\n```\n\n## No animations not driven by `useCurrentFrame()`\n\nShaders, models etc MUST NOT animate by themselves.  \nNo animations are allowed unless they are driven by `useCurrentFrame()`.  \nOtherwise, it will cause flickering during rendering.  \n\nUsing `useFrame()` from `@react-three/fiber` is forbidden.\n\n## Animate using `useCurrentFrame()`\n\nUse `useCurrentFrame()` to perform animations.\n\n```tsx\nconst frame = useCurrentFrame();\nconst rotationY = frame * 0.02;\n\n<mesh rotation={[0, rotationY, 0]}>\n  <boxGeometry args={[2, 2, 2]} />\n  <meshStandardMaterial color=\"#4a9eff\" />\n</mesh>\n```\n\n## Using `<Sequence>` inside `<ThreeCanvas>`\n\nThe `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.\n\n```tsx\nimport { Sequence } from \"remotion\";\nimport { ThreeCanvas } from \"@remotion/three\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <Sequence layout=\"none\">\n    <mesh>\n      <boxGeometry args={[2, 2, 2]} />\n      <meshStandardMaterial color=\"#4a9eff\" />\n    </mesh>\n  </Sequence>\n</ThreeCanvas>\n```","skills/remotion-best-practices/rules/animations.md":"---\nname: animations\ndescription: Fundamental animation skills for Remotion\nmetadata:\n  tags: animations, transitions, frames, useCurrentFrame\n---\n\nAll animations MUST be driven by the `useCurrentFrame()` hook.  \nWrite animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.\n\n```tsx\nimport { useCurrentFrame } from \"remotion\";\n\nexport const FadeIn = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {\n    extrapolateRight: 'clamp',\n  });\n \n  return (\n    <div style={{ opacity }}>Hello World!</div>\n  );\n};\n```\n\nCSS transitions or animations are FORBIDDEN - they will not render correctly.  \nTailwind animation class names are FORBIDDEN - they will not render correctly.  ","skills/remotion-best-practices/rules/assets.md":"---\nname: assets\ndescription: Importing images, videos, audio, and fonts into Remotion\nmetadata:\n  tags: assets, staticFile, images, fonts, public\n---\n\n# Importing assets in Remotion\n\n## The public folder\n\nPlace assets in the `public/` folder at your project root.\n\n## Using staticFile()\n\nYou MUST use `staticFile()` to reference files from the `public/` folder:\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\nexport const MyComposition = () => {\n  return <Img src={staticFile('logo.png')} />;\n};\n```\n\nThe function returns an encoded URL that works correctly when deploying to subdirectories.\n\n## Using with components\n\n**Images:**\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\n<Img src={staticFile('photo.png')} />;\n```\n\n**Videos:**\n\n```tsx\nimport {Video} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Video src={staticFile('clip.mp4')} />;\n```\n\n**Audio:**\n\n```tsx\nimport {Audio} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Audio src={staticFile('music.mp3')} />;\n```\n\n**Fonts:**\n\n```tsx\nimport {staticFile} from 'remotion';\n\nconst fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);\nawait fontFamily.load();\ndocument.fonts.add(fontFamily);\n```\n\n## Remote URLs\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n<Video src=\"https://remotion.media/video.mp4\" />\n```\n\n## Important notes\n\n- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering\n- Special characters in filenames (`#`, `?`, `&`) are automatically encoded\n","skills/remotion-best-practices/rules/audio.md":"---\nname: audio\ndescription: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch\nmetadata:\n  tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx\n---\n\n# Using audio in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/media package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/media\n```\n\n## Importing Audio\n\nUse `<Audio>` from `@remotion/media` to add audio to your composition.\n\n```tsx\nimport { Audio } from \"@remotion/media\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Audio src={staticFile(\"audio.mp3\")} />;\n};\n```\n\nRemote URLs are also supported:\n\n```tsx\n<Audio src=\"https://remotion.media/audio.mp3\" />\n```\n\nBy default, audio plays from the start, at full volume and full length.\nMultiple audio tracks can be layered by adding multiple `<Audio>` components.\n\n## Trimming\n\nUse `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.\n\n```tsx\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    trimBefore={2 * fps} // Skip the first 2 seconds\n    trimAfter={10 * fps} // End at the 10 second mark\n  />\n);\n```\n\nThe audio still starts playing at the beginning of the composition - only the specified portion is played.\n\n## Delaying\n\nWrap the audio in a `<Sequence>` to delay when it starts:\n\n```tsx\nimport { Sequence, staticFile } from \"remotion\";\nimport { Audio } from \"@remotion/media\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Sequence from={1 * fps}>\n    <Audio src={staticFile(\"audio.mp3\")} />\n  </Sequence>\n);\n```\n\nThe audio will start playing after 1 second.\n\n## Volume\n\nSet a static volume (0 to 1):\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} volume={0.5} />\n```\n\nOr use a callback for dynamic volume based on the current frame:\n\n```tsx\nimport { interpolate } from \"remotion\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    volume={(f) =>\n      interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: \"clamp\" })\n    }\n  />\n);\n```\n\nThe value of `f` starts at 0 when the audio begins to play, not the composition frame.\n\n## Muting\n\nUse `muted` to silence the audio. It can be set dynamically:\n\n```tsx\nconst frame = useCurrentFrame();\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s\n  />\n);\n```\n\n## Speed\n\nUse `playbackRate` to change the playback speed:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={2} /> {/* 2x speed */}\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={0.5} /> {/* Half speed */}\n```\n\nReverse playback is not supported.\n\n## Looping\n\nUse `loop` to loop the audio indefinitely:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} loop />\n```\n\nUse `loopVolumeCurveBehavior` to control how the frame count behaves when looping:\n\n- `\"repeat\"`: Frame count resets to 0 each loop (default)\n- `\"extend\"`: Frame count continues incrementing\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  loop\n  loopVolumeCurveBehavior=\"extend\"\n  volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops\n/>\n```\n\n## Pitch\n\nUse `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={1.5} // Higher pitch\n/>\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={0.8} // Lower pitch\n/>\n```\n\nPitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.\n","skills/remotion-best-practices/rules/calculate-metadata.md":"---\nname: calculate-metadata\ndescription: Dynamically set composition duration, dimensions, and props\nmetadata:\n  tags: calculateMetadata, duration, dimensions, props, dynamic\n---\n\n# Using calculateMetadata\n\nUse `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.\n\n```tsx\n<Composition id=\"MyComp\" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />\n```\n\n## Setting duration based on a video\n\nUse the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:\n\n```tsx\nimport {CalculateMetadataFunction} from 'remotion';\nimport {getMediaMetadata} from '../get-media-metadata';\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n  };\n};\n```\n\n## Matching dimensions of a video\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n    width: dimensions?.width ?? 1920,\n    height: dimensions?.height ?? 1080,\n  };\n};\n```\n\n## Setting duration based on multiple videos\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));\n  const allMetadata = await Promise.all(metadataPromises);\n\n  const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);\n\n  return {\n    durationInFrames: Math.ceil(totalDuration * 30),\n  };\n};\n```\n\n## Setting a default outName\n\nSet the default output filename based on props:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  return {\n    defaultOutName: `video-${props.id}.mp4`,\n  };\n};\n```\n\n## Transforming props\n\nFetch data or transform props before rendering:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {\n  const response = await fetch(props.dataUrl, {signal: abortSignal});\n  const data = await response.json();\n\n  return {\n    props: {\n      ...props,\n      fetchedData: data,\n    },\n  };\n};\n```\n\nThe `abortSignal` cancels stale requests when props change in the Studio.\n\n## Return value\n\nAll fields are optional. Returned values override the `<Composition>` props:\n\n- `durationInFrames`: Number of frames\n- `width`: Composition width in pixels\n- `height`: Composition height in pixels\n- `fps`: Frames per second\n- `props`: Transformed props passed to the component\n- `defaultOutName`: Default output filename\n- `defaultCodec`: Default codec for rendering\n","skills/remotion-best-practices/rules/can-decode.md":"---\nname: can-decode\ndescription: Check if a video can be decoded by the browser using Mediabunny\nmetadata:\n  tags: decode, validation, video, audio, compatibility, browser\n---\n\n# Checking if a video can be decoded\n\nUse Mediabunny to check if a video can be decoded by the browser before attempting to play it.\n\n## The `canDecode()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const canDecode = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  try {\n    await input.getFormat();\n  } catch {\n    return false;\n  }\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (videoTrack && !(await videoTrack.canDecode())) {\n    return false;\n  }\n\n  const audioTrack = await input.getPrimaryAudioTrack();\n  if (audioTrack && !(await audioTrack.canDecode())) {\n    return false;\n  }\n\n  return true;\n};\n```\n\n## Usage\n\n```tsx\nconst src = \"https://remotion.media/video.mp4\";\nconst isDecodable = await canDecode(src);\n\nif (isDecodable) {\n  console.log(\"Video can be decoded\");\n} else {\n  console.log(\"Video cannot be decoded by this browser\");\n}\n```\n\n## Using with Blob\n\nFor file uploads or drag-and-drop, use `BlobSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, BlobSource } from \"mediabunny\";\n\nexport const canDecodeBlob = async (blob: Blob) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new BlobSource(blob),\n  });\n\n  // Same validation logic as above\n};\n```\n","skills/remotion-best-practices/rules/charts.md":"---\nname: charts\ndescription: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.\nmetadata:\n  tags: charts, data, visualization, bar-chart, pie-chart, graphs\n---\n\n# Charts in Remotion\n\nYou can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.\n\n## No animations not powered by `useCurrentFrame()`\n\nDisable all animations by third party libraries.  \nThey will cause flickering during rendering.  \nInstead, drive all animations from `useCurrentFrame()`.\n\n## Bar Chart Animations\n\nSee [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.\n\n### Staggered Bars\n\nYou can animate the height of the bars and stagger them like this:\n\n```tsx\nconst STAGGER_DELAY = 5;\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst bars = data.map((item, i) => {\n  const delay = i * STAGGER_DELAY;\n  const height = spring({\n    frame,\n    fps,\n    delay,\n    config: {damping: 200},\n  });\n  return <div style={{height: height * item.value}} />;\n});\n```\n\n## Pie Chart Animation\n\nAnimate segments using stroke-dashoffset, starting from 12 o'clock.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst progress = interpolate(frame, [0, 100], [0, 1]);\n\nconst circumference = 2 * Math.PI * radius;\nconst segmentLength = (value / total) * circumference;\nconst offset = interpolate(progress, [0, 1], [segmentLength, 0]);\n\n<circle r={radius} cx={center} cy={center} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;\n```\n","skills/remotion-best-practices/rules/compositions.md":"---\nname: compositions\ndescription: Defining compositions, stills, folders, default props and dynamic metadata\nmetadata:\n  tags: composition, still, folder, props, metadata\n---\n\nA `<Composition>` defines the component, width, height, fps and duration of a renderable video.\n\nIt normally is placed in the `src/Root.tsx` file.\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;\n};\n```\n\n## Default Props\n\nPass `defaultProps` to provide initial values for your component.  \nValues must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100}\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={\n        {\n          title: 'Hello World',\n          color: '#ff0000',\n        } satisfies MyCompositionProps\n      }\n    />\n  );\n};\n```\n\nUse `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.\n\n## Folders\n\nUse `<Folder>` to organize compositions in the sidebar.  \nFolder names can only contain letters, numbers, and hyphens.\n\n```tsx\nimport {Composition, Folder} from 'remotion';\n\nexport const RemotionRoot = () => {\n  return (\n    <>\n      <Folder name=\"Marketing\">\n        <Composition id=\"Promo\" /* ... */ />\n        <Composition id=\"Ad\" /* ... */ />\n      </Folder>\n      <Folder name=\"Social\">\n        <Folder name=\"Instagram\">\n          <Composition id=\"Story\" /* ... */ />\n          <Composition id=\"Reel\" /* ... */ />\n        </Folder>\n      </Folder>\n    </>\n  );\n};\n```\n\n## Stills\n\nUse `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.\n\n```tsx\nimport {Still} from 'remotion';\nimport {Thumbnail} from './Thumbnail';\n\nexport const RemotionRoot = () => {\n  return <Still id=\"Thumbnail\" component={Thumbnail} width={1280} height={720} />;\n};\n```\n\n## Calculate Metadata\n\nUse `calculateMetadata` to make dimensions, duration, or props dynamic based on data.\n\n```tsx\nimport {Composition, CalculateMetadataFunction} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nconst calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {\n  const data = await fetch(`https://api.example.com/video/${props.videoId}`, {\n    signal: abortSignal,\n  }).then((res) => res.json());\n\n  return {\n    durationInFrames: Math.ceil(data.duration * 30),\n    props: {\n      ...props,\n      videoUrl: data.url,\n    },\n  };\n};\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100} // Placeholder, will be overridden\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={{videoId: 'abc123'}}\n      calculateMetadata={calculateMetadata}\n    />\n  );\n};\n```\n\nThe function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/display-captions.md":"---\nname: display-captions\ndescription: Displaying captions in Remotion with TikTok-style pages and word highlighting\nmetadata:\n  tags: captions, subtitles, display, tiktok, highlight\n---\n\n# Displaying captions in Remotion\n\nThis guide explains how to display captions in Remotion, assuming you already have captions in the [`Caption`](https://www.remotion.dev/docs/captions/caption) format.\n\n## Prerequisites\n\nRead [Transcribing audio](transcribe-captions.md) for how to generate captions.\n\nFirst, the [`@remotion/captions`](https://www.remotion.dev/docs/captions) package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions\n```\n\n## Fetching captions\n\nFirst, fetch your captions JSON file. Use [`useDelayRender()`](https://www.remotion.dev/docs/use-delay-render) to hold the render until the captions are loaded:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      // Assuming captions.json is in the public/ folder.\n      const response = await fetch(staticFile(\"captions123.json\"));\n      const data = await response.json();\n      setCaptions(data);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;\n};\n```\n\n## Creating pages\n\nUse `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:\n\n```tsx\nimport { useMemo } from \"react\";\nimport { createTikTokStyleCaptions } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\n// How often captions should switch (in milliseconds)\n// Higher values = more words per page\n// Lower values = fewer words (more word-by-word)\nconst SWITCH_CAPTIONS_EVERY_MS = 1200;\n\nconst { pages } = useMemo(() => {\n  return createTikTokStyleCaptions({\n    captions,\n    combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,\n  });\n}, [captions]);\n```\n\n## Rendering with Sequences\n\nMap over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:\n\n```tsx\nimport { Sequence, useVideoConfig, AbsoluteFill } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst CaptionedContent: React.FC = () => {\n  const { fps } = useVideoConfig();\n\n  return (\n    <AbsoluteFill>\n      {pages.map((page, index) => {\n        const nextPage = pages[index + 1] ?? null;\n        const startFrame = (page.startMs / 1000) * fps;\n        const endFrame = Math.min(\n          nextPage ? (nextPage.startMs / 1000) * fps : Infinity,\n          startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,\n        );\n        const durationInFrames = endFrame - startFrame;\n\n        if (durationInFrames <= 0) {\n          return null;\n        }\n\n        return (\n          <Sequence\n            key={index}\n            from={startFrame}\n            durationInFrames={durationInFrames}\n          >\n            <CaptionPage page={page} />\n          </Sequence>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n```\n\n## White-space preservation\n\nThe captions are whitespace sensitive. You should include spaces in the `text` field before each word. Use `whiteSpace: \"pre\"` to preserve the whitespace in the captions.\n\n## Separate component for captions\n\nPut captioning logic in a separate component.  \nMake a new file for it.\n\n## Word highlighting\n\nA caption page contains `tokens` which you can use to highlight the currently spoken word:\n\n```tsx\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst HIGHLIGHT_COLOR = \"#39E508\";\n\nconst CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Current time relative to the start of the sequence\n  const currentTimeMs = (frame / fps) * 1000;\n  // Convert to absolute time by adding the page start\n  const absoluteTimeMs = page.startMs + currentTimeMs;\n\n  return (\n    <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n      <div style={{ fontSize: 80, fontWeight: \"bold\", whiteSpace: \"pre\" }}>\n        {page.tokens.map((token) => {\n          const isActive =\n            token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;\n\n          return (\n            <span\n              key={token.fromMs}\n              style={{ color: isActive ? HIGHLIGHT_COLOR : \"white\" }}\n            >\n              {token.text}\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n```\n\n## Display captions alongside video content\n\nBy default, put the captions alongside the video content, so the captions are in sync.  \nFor each video, make a new captions JSON file.\n\n```tsx\n<AbsoluteFill>\n  <Video src={staticFile(\"video.mp4\")} />\n  <CaptionPage page={page} />\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/extract-frames.md":"---\nname: extract-frames\ndescription: Extract frames from videos at specific timestamps using Mediabunny\nmetadata:\n  tags: frames, extract, video, thumbnail, filmstrip, canvas\n---\n\n# Extracting frames from videos\n\nUse Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.\n\n## The `extractFrames()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport {\n  ALL_FORMATS,\n  Input,\n  UrlSource,\n  VideoSample,\n  VideoSampleSink,\n} from \"mediabunny\";\n\ntype Options = {\n  track: { width: number; height: number };\n  container: string;\n  durationInSeconds: number | null;\n};\n\nexport type ExtractFramesTimestampsInSecondsFn = (\n  options: Options\n) => Promise<number[]> | number[];\n\nexport type ExtractFramesProps = {\n  src: string;\n  timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;\n  onVideoSample: (sample: VideoSample) => void;\n  signal?: AbortSignal;\n};\n\nexport async function extractFrames({\n  src,\n  timestampsInSeconds,\n  onVideoSample,\n  signal,\n}: ExtractFramesProps): Promise<void> {\n  using input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src),\n  });\n\n  const [durationInSeconds, format, videoTrack] = await Promise.all([\n    input.computeDuration(),\n    input.getFormat(),\n    input.getPrimaryVideoTrack(),\n  ]);\n\n  if (!videoTrack) {\n    throw new Error(\"No video track found in the input\");\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const timestamps =\n    typeof timestampsInSeconds === \"function\"\n      ? await timestampsInSeconds({\n          track: {\n            width: videoTrack.displayWidth,\n            height: videoTrack.displayHeight,\n          },\n          container: format.name,\n          durationInSeconds,\n        })\n      : timestampsInSeconds;\n\n  if (timestamps.length === 0) {\n    return;\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const sink = new VideoSampleSink(videoTrack);\n\n  for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {\n    if (signal?.aborted) {\n      break;\n    }\n\n    if (!videoSample) {\n      continue;\n    }\n\n    onVideoSample(videoSample);\n  }\n}\n```\n\n## Basic usage\n\nExtract frames at specific timestamps:\n\n```tsx\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: [0, 1, 2, 3, 4],\n  onVideoSample: (sample) => {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Creating a filmstrip\n\nUse a callback function to dynamically calculate timestamps based on video metadata:\n\n```tsx\nconst canvasWidth = 500;\nconst canvasHeight = 80;\nconst fromSeconds = 0;\nconst toSeconds = 10;\n\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: async ({ track, durationInSeconds }) => {\n    const aspectRatio = track.width / track.height;\n    const amountOfFramesFit = Math.ceil(\n      canvasWidth / (canvasHeight * aspectRatio)\n    );\n    const segmentDuration = toSeconds - fromSeconds;\n    const timestamps: number[] = [];\n\n    for (let i = 0; i < amountOfFramesFit; i++) {\n      timestamps.push(\n        fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)\n      );\n    }\n\n    return timestamps;\n  },\n  onVideoSample: (sample) => {\n    console.log(`Frame at ${sample.timestamp}s`);\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Cancellation with AbortSignal\n\nCancel frame extraction after a timeout:\n\n```tsx\nconst controller = new AbortController();\n\nsetTimeout(() => controller.abort(), 5000);\n\ntry {\n  await extractFrames({\n    src: \"https://remotion.media/video.mp4\",\n    timestampsInSeconds: [0, 1, 2, 3, 4],\n    onVideoSample: (sample) => {\n      using frame = sample;\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = frame.displayWidth;\n      canvas.height = frame.displayHeight;\n      const ctx = canvas.getContext(\"2d\");\n      frame.draw(ctx!, 0, 0);\n    },\n    signal: controller.signal,\n  });\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n\n## Timeout with Promise.race\n\n```tsx\nconst controller = new AbortController();\n\nconst timeoutPromise = new Promise<never>((_, reject) => {\n  const timeoutId = setTimeout(() => {\n    controller.abort();\n    reject(new Error(\"Frame extraction timed out after 10 seconds\"));\n  }, 10000);\n\n  controller.signal.addEventListener(\"abort\", () => clearTimeout(timeoutId), {\n    once: true,\n  });\n});\n\ntry {\n  await Promise.race([\n    extractFrames({\n      src: \"https://remotion.media/video.mp4\",\n      timestampsInSeconds: [0, 1, 2, 3, 4],\n      onVideoSample: (sample) => {\n        using frame = sample;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = frame.displayWidth;\n        canvas.height = frame.displayHeight;\n        const ctx = canvas.getContext(\"2d\");\n        frame.draw(ctx!, 0, 0);\n      },\n      signal: controller.signal,\n    }),\n    timeoutPromise,\n  ]);\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n","skills/remotion-best-practices/rules/fonts.md":"---\nname: fonts\ndescription: Loading Google Fonts and local fonts in Remotion\nmetadata:\n  tags: fonts, google-fonts, typography, text\n---\n\n# Using fonts in Remotion\n\n## Google Fonts with @remotion/google-fonts\n\nThe recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.\n\n### Prerequisites\n\nFirst, the @remotion/google-fonts package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/google-fonts # If project uses npm\nbunx remotion add @remotion/google-fonts # If project uses bun\nyarn remotion add @remotion/google-fonts # If project uses yarn\npnpm exec remotion add @remotion/google-fonts # If project uses pnpm\n```\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily } = loadFont();\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily }}>Hello World</div>;\n};\n```\n\nPreferrably, specify only needed weights and subsets to reduce file size:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Roboto\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n```\n\n### Waiting for font to load\n\nUse `waitUntilDone()` if you need to know when the font is ready:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily, waitUntilDone } = loadFont();\n\nawait waitUntilDone();\n```\n\n## Local fonts with @remotion/fonts\n\nFor local font files, use the `@remotion/fonts` package.\n\n### Prerequisites\n\nFirst, install @remotion/fonts:\n\n```bash\nnpx remotion add @remotion/fonts # If project uses npm\nbunx remotion add @remotion/fonts # If project uses bun\nyarn remotion add @remotion/fonts # If project uses yarn\npnpm exec remotion add @remotion/fonts # If project uses pnpm\n```\n\n### Loading a local font\n\nPlace your font file in the `public/` folder and use `loadFont()`:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait loadFont({\n  family: \"MyFont\",\n  url: staticFile(\"MyFont-Regular.woff2\"),\n});\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily: \"MyFont\" }}>Hello World</div>;\n};\n```\n\n### Loading multiple weights\n\nLoad each weight separately with the same family name:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait Promise.all([\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Regular.woff2\"),\n    weight: \"400\",\n  }),\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Bold.woff2\"),\n    weight: \"700\",\n  }),\n]);\n```\n\n### Available options\n\n```tsx\nloadFont({\n  family: \"MyFont\", // Required: name to use in CSS\n  url: staticFile(\"font.woff2\"), // Required: font file URL\n  format: \"woff2\", // Optional: auto-detected from extension\n  weight: \"400\", // Optional: font weight\n  style: \"normal\", // Optional: normal or italic\n  display: \"block\", // Optional: font-display behavior\n});\n```\n\n## Using in components\n\nCall `loadFont()` at the top level of your component or in a separate file that's imported early:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Montserrat\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport const Title: React.FC<{ text: string }> = ({ text }) => {\n  return (\n    <h1\n      style={{\n        fontFamily,\n        fontSize: 80,\n        fontWeight: \"bold\",\n      }}\n    >\n      {text}\n    </h1>\n  );\n};\n```\n","skills/remotion-best-practices/rules/get-audio-duration.md":"---\nname: get-audio-duration\ndescription: Getting the duration of an audio file in seconds with Mediabunny\nmetadata:\n  tags: duration, audio, length, time, seconds, mp3, wav\n---\n\n# Getting audio duration with Mediabunny\n\nMediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.\n\n## Getting audio duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getAudioDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getAudioDuration(\"https://remotion.media/audio.mp3\");\nconsole.log(duration); // e.g. 180.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getAudioDuration(staticFile(\"audio.mp3\"));\n```\n","skills/remotion-best-practices/rules/get-video-dimensions.md":"---\nname: get-video-dimensions\ndescription: Getting the width and height of a video file with Mediabunny\nmetadata:\n  tags: dimensions, width, height, resolution, size, video\n---\n\n# Getting video dimensions with Mediabunny\n\nMediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video dimensions\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDimensions = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (!videoTrack) {\n    throw new Error(\"No video track found\");\n  }\n\n  return {\n    width: videoTrack.displayWidth,\n    height: videoTrack.displayHeight,\n  };\n};\n```\n\n## Usage\n\n```tsx\nconst dimensions = await getVideoDimensions(\"https://remotion.media/video.mp4\");\nconsole.log(dimensions.width);  // e.g. 1920\nconsole.log(dimensions.height); // e.g. 1080\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst videoTrack = await input.getPrimaryVideoTrack();\nconst width = videoTrack.displayWidth;\nconst height = videoTrack.displayHeight;\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst dimensions = await getVideoDimensions(staticFile(\"video.mp4\"));\n```\n","skills/remotion-best-practices/rules/get-video-duration.md":"---\nname: get-video-duration\ndescription: Getting the duration of a video file in seconds with Mediabunny\nmetadata:\n  tags: duration, video, length, time, seconds\n---\n\n# Getting video duration with Mediabunny\n\nMediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getVideoDuration(\"https://remotion.media/video.mp4\");\nconsole.log(duration); // e.g. 10.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getVideoDuration(staticFile(\"video.mp4\"));\n```\n","skills/remotion-best-practices/rules/gifs.md":"---\nname: gif\ndescription: Displaying GIFs, APNG, AVIF and WebP in Remotion\nmetadata:\n  tags: gif, animation, images, animated, apng, avif, webp\n---\n\n# Using Animated images in Remotion\n\n## Basic usage\n\nUse `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:\n\n```tsx\nimport { AnimatedImage, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return (\n    <AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} />\n  );\n};\n```\n\nRemote URLs are also supported (must have CORS enabled):\n\n```tsx\n<AnimatedImage\n  src=\"https://example.com/animation.gif\"\n  width={500}\n  height={500}\n/>\n```\n\n## Sizing and fit\n\nControl how the image fills its container with the `fit` prop:\n\n```tsx\n// Stretch to fill (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"fill\" />\n\n// Maintain aspect ratio, fit inside container\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"contain\" />\n\n// Fill container, crop if needed\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"cover\" />\n```\n\n## Playback speed\n\nUse `playbackRate` to control the animation speed:\n\n```tsx\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}\n```\n\n## Looping behavior\n\nControl what happens when the animation finishes:\n\n```tsx\n// Loop indefinitely (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"loop\" />\n\n// Play once, show final frame\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"pause-after-finish\" />\n\n// Play once, then clear canvas\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"clear-after-finish\" />\n```\n\n## Styling\n\nUse the `style` prop for additional CSS (use `width` and `height` props for sizing):\n\n```tsx\n<AnimatedImage\n  src={staticFile(\"animation.gif\")}\n  width={500}\n  height={500}\n  style={{\n    borderRadius: 20,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n  }}\n/>\n```\n\n## Getting GIF duration\n\nUse `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.\n\n```bash\nnpx remotion add @remotion/gif\n```\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nconst duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\nconsole.log(duration); // e.g. 2.5\n```\n\nThis is useful for setting the composition duration to match the GIF:\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\n  return {\n    durationInFrames: Math.ceil(duration * 30),\n  };\n};\n```\n\n## Alternative\n\nIf `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.\n\n```bash\nnpx remotion add @remotion/gif # If project uses npm\nbunx remotion add @remotion/gif # If project uses bun\nyarn remotion add @remotion/gif # If project uses yarn\npnpm exec remotion add @remotion/gif # If project uses pnpm\n```\n\n```tsx\nimport { Gif } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Gif src={staticFile(\"animation.gif\")} width={500} height={500} />;\n};\n```\n\nThe `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.\n","skills/remotion-best-practices/rules/images.md":"---\nname: images\ndescription: Embedding images in Remotion using the <Img> component\nmetadata:\n  tags: images, img, staticFile, png, jpg, svg, webp\n---\n\n# Using images in Remotion\n\n## The `<Img>` component\n\nAlways use the `<Img>` component from `remotion` to display images:\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Img src={staticFile(\"photo.png\")} />;\n};\n```\n\n## Important restrictions\n\n**You MUST use the `<Img>` component from `remotion`.** Do not use:\n\n- Native HTML `<img>` elements\n- Next.js `<Image>` component\n- CSS `background-image`\n\nThe `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.\n\n## Local images with staticFile()\n\nPlace images in the `public/` folder and use `staticFile()` to reference them:\n\n```\nmy-video/\n├─ public/\n│  ├─ logo.png\n│  ├─ avatar.jpg\n│  └─ icon.svg\n├─ src/\n├─ package.json\n```\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\n<Img src={staticFile(\"logo.png\")} />\n```\n\n## Remote images\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n```\n\nEnsure remote images have CORS enabled.\n\nFor animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.\n\n## Sizing and positioning\n\nUse the `style` prop to control size and position:\n\n```tsx\n<Img\n  src={staticFile(\"photo.png\")}\n  style={{\n    width: 500,\n    height: 300,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n    objectFit: \"cover\",\n  }}\n/>\n```\n\n## Dynamic image paths\n\nUse template literals for dynamic file references:\n\n```tsx\nimport { Img, staticFile, useCurrentFrame } from \"remotion\";\n\nconst frame = useCurrentFrame();\n\n// Image sequence\n<Img src={staticFile(`frames/frame${frame}.png`)} />\n\n// Selecting based on props\n<Img src={staticFile(`avatars/${props.userId}.png`)} />\n\n// Conditional images\n<Img src={staticFile(`icons/${isActive ? \"active\" : \"inactive\"}.svg`)} />\n```\n\nThis pattern is useful for:\n\n- Image sequences (frame-by-frame animations)\n- User-specific avatars or profile images\n- Theme-based icons\n- State-dependent graphics\n\n## Getting image dimensions\n\nUse `getImageDimensions()` to get the dimensions of an image:\n\n```tsx\nimport { getImageDimensions, staticFile } from \"remotion\";\n\nconst { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n```\n\nThis is useful for calculating aspect ratios or sizing compositions:\n\n```tsx\nimport { getImageDimensions, staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n  return {\n    width,\n    height,\n  };\n};\n```\n","skills/remotion-best-practices/rules/import-srt-captions.md":"---\nname: import-srt-captions\ndescription: Importing .srt subtitle files into Remotion using @remotion/captions\nmetadata:\n  tags: captions, subtitles, srt, import, parse\n---\n\n# Importing .srt subtitles into Remotion\n\nIf you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.\n\nIf you don't have a .srt file, read [Transcribing audio](transcribe-captions.md) for how to generate captions instead.\n\n## Prerequisites\n\nFirst, the @remotion/captions package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions # If project uses npm\nbunx remotion add @remotion/captions # If project uses bun\nyarn remotion add @remotion/captions # If project uses yarn\npnpm exec remotion add @remotion/captions # If project uses pnpm\n```\n\n## Reading an .srt file\n\nUse `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport { parseSrt } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      const response = await fetch(staticFile(\"subtitles.srt\"));\n      const text = await response.text();\n      const { captions: parsed } = parseSrt({ input: text });\n      setCaptions(parsed);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;\n};\n```\n\nRemote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.\n\n## Using imported captions\n\nOnce parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.\n","skills/remotion-best-practices/rules/light-leaks.md":"---\nname: light-leaks\ndescription: Light leak overlay effects for Remotion using @remotion/light-leaks.\nmetadata:\n  tags: light-leaks, overlays, effects, transitions\n---\n\n## Light Leaks\n\nThis only works from Remotion 4.0.415 and up. Use `npx remotion versions` to check your Remotion version and `npx remotion upgrade` to upgrade your Remotion version.\n\n`<LightLeak>` from `@remotion/light-leaks` renders a WebGL-based light leak effect. It reveals during the first half of its duration and retracts during the second half.\n\nTypically used inside a `<TransitionSeries.Overlay>` to play over the cut point between two scenes. See the **transitions** rule for `<TransitionSeries>` and overlay usage.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/light-leaks\n```\n\n## Basic usage with TransitionSeries\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Props\n\n- `durationInFrames?` — defaults to the parent sequence/composition duration. The effect reveals during the first half and retracts during the second half.\n- `seed?` — determines the shape of the light leak pattern. Different seeds produce different patterns. Default: `0`.\n- `hueShift?` — rotates the hue in degrees (`0`–`360`). Default: `0` (yellow-to-orange). `120` = green, `240` = blue.\n\n## Customizing the look\n\n```tsx\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n// Blue-tinted light leak with a different pattern\n<LightLeak seed={5} hueShift={240} />;\n\n// Green-tinted light leak\n<LightLeak seed={2} hueShift={120} />;\n```\n\n## Standalone usage\n\n`<LightLeak>` can also be used outside of `<TransitionSeries>`, for example as a decorative overlay in any composition:\n\n```tsx\nimport { AbsoluteFill } from \"remotion\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\nconst MyComp: React.FC = () => (\n  <AbsoluteFill>\n    <MyContent />\n    <LightLeak durationInFrames={60} seed={3} />\n  </AbsoluteFill>\n);\n```\n","skills/remotion-best-practices/rules/lottie.md":"---\nname: lottie\ndescription: Embedding Lottie animations in Remotion.\nmetadata:\n  category: Animation\n---\n\n# Using Lottie Animations in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/lottie package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/lottie # If project uses npm\nbunx remotion add @remotion/lottie # If project uses bun\nyarn remotion add @remotion/lottie # If project uses yarn\npnpm exec remotion add @remotion/lottie # If project uses pnpm\n```\n\n## Displaying a Lottie file\n\nTo import a Lottie animation:\n\n- Fetch the Lottie asset\n- Wrap the loading process in `delayRender()` and `continueRender()`\n- Save the animation data in a state\n- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package\n\n```tsx\nimport {Lottie, LottieAnimationData} from '@remotion/lottie';\nimport {useEffect, useState} from 'react';\nimport {cancelRender, continueRender, delayRender} from 'remotion';\n\nexport const MyAnimation = () => {\n  const [handle] = useState(() => delayRender('Loading Lottie animation'));\n\n  const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);\n\n  useEffect(() => {\n    fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')\n      .then((data) => data.json())\n      .then((json) => {\n        setAnimationData(json);\n        continueRender(handle);\n      })\n      .catch((err) => {\n        cancelRender(err);\n      });\n  }, [handle]);\n\n  if (!animationData) {\n    return null;\n  }\n\n  return <Lottie animationData={animationData} />;\n};\n```\n\n## Styling and animating\n\nLottie supports the `style` prop to allow styles and animations:\n\n```tsx\nreturn <Lottie animationData={animationData} style={{width: 400, height: 400}} />;\n```\n\n","skills/remotion-best-practices/rules/maps.md":"---\nname: maps\ndescription: Make map animations with Mapbox\nmetadata:\n  tags: map, map animation, mapbox\n---\n\nMaps can be added to a Remotion video with Mapbox.  \nThe [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.\n\n## Prerequisites\n\nMapbox and `@turf/turf` need to be installed.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `bun.lock` is found, use the following command:\n\n```bash\nbun i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nThe user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.\n\nThe mapbox token needs to be added to the `.env` file:\n\n```txt title=\".env\"\nREMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token\n```\n\n## Adding a map\n\nHere is a basic example of a map in Remotion.\n\n```tsx\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';\nimport mapboxgl, {Map} from 'mapbox-gl';\n\nexport const lineCoordinates = [\n  [6.56158447265625, 46.059891147620725],\n  [6.5691375732421875, 46.05679376154153],\n  [6.5842437744140625, 46.05059898938315],\n  [6.594886779785156, 46.04702502069337],\n  [6.601066589355469, 46.0460718554722],\n  [6.6089630126953125, 46.0365370783104],\n  [6.6185760498046875, 46.018420689207964],\n];\n\nmapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;\n\nexport const MyComposition = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const {delayRender, continueRender} = useDelayRender();\n\n  const {width, height} = useVideoConfig();\n  const [handle] = useState(() => delayRender('Loading map...'));\n  const [map, setMap] = useState<Map | null>(null);\n\n  useEffect(() => {\n    const _map = new Map({\n      container: ref.current!,\n      zoom: 11.53,\n      center: [6.5615, 46.0598],\n      pitch: 65,\n      bearing: 0,\n      style: '⁠mapbox://styles/mapbox/standard',\n      interactive: false,\n      fadeDuration: 0,\n    });\n\n    _map.on('style.load', () => {\n      // Hide all features from the Mapbox Standard style\n      const hideFeatures = [\n        'showRoadsAndTransit',\n        'showRoads',\n        'showTransit',\n        'showPedestrianRoads',\n        'showRoadLabels',\n        'showTransitLabels',\n        'showPlaceLabels',\n        'showPointOfInterestLabels',\n        'showPointsOfInterest',\n        'showAdminBoundaries',\n        'showLandmarkIcons',\n        'showLandmarkIconLabels',\n        'show3dObjects',\n        'show3dBuildings',\n        'show3dTrees',\n        'show3dLandmarks',\n        'show3dFacades',\n      ];\n      for (const feature of hideFeatures) {\n        _map.setConfigProperty('basemap', feature, false);\n      }\n\n      _map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');\n\n      _map.addSource('trace', {\n        type: 'geojson',\n        data: {\n          type: 'Feature',\n          properties: {},\n          geometry: {\n            type: 'LineString',\n            coordinates: lineCoordinates,\n          },\n        },\n      });\n      _map.addLayer({\n        type: 'line',\n        source: 'trace',\n        id: 'line',\n        paint: {\n          'line-color': 'black',\n          'line-width': 5,\n        },\n        layout: {\n          'line-cap': 'round',\n          'line-join': 'round',\n        },\n      });\n    });\n\n    _map.on('load', () => {\n      continueRender(handle);\n      setMap(_map);\n    });\n  }, [handle, lineCoordinates]);\n\n  const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);\n\n  return <AbsoluteFill ref={ref} style={style} />;\n};\n```\n\nThe following is important in Remotion:\n\n- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.\n- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.\n- The element containing the ref MUST have an explicit width and height and `position: \"absolute\"`.\n- Do not add a `_map.remove();` cleanup function.\n\n## Drawing lines\n\nUnless I request it, do not add a glow effect to the lines.\nUnless I request it, do not add additional points to the lines.\n\n## Map style\n\nBy default, use the `mapbox://styles/mapbox/standard` style.  \nHide the labels from the base map style.\n\nUnless I request otherwise, remove all features from the Mapbox Standard style.\n\n```tsx\n// Hide all features from the Mapbox Standard style\nconst hideFeatures = [\n  'showRoadsAndTransit',\n  'showRoads',\n  'showTransit',\n  'showPedestrianRoads',\n  'showRoadLabels',\n  'showTransitLabels',\n  'showPlaceLabels',\n  'showPointOfInterestLabels',\n  'showPointsOfInterest',\n  'showAdminBoundaries',\n  'showLandmarkIcons',\n  'showLandmarkIconLabels',\n  'show3dObjects',\n  'show3dBuildings',\n  'show3dTrees',\n  'show3dLandmarks',\n  'show3dFacades',\n];\nfor (const feature of hideFeatures) {\n  _map.setConfigProperty('basemap', feature, false);\n}\n\n_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');\n_map.setConfigProperty('basemap', 'colorRoads', 'transparent');\n_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');\n```\n\n## Animating the camera\n\nYou can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.\n\nUnless I ask for it, do not jump between camera angles.\n\n```tsx\nimport * as turf from '@turf/turf';\nimport {interpolate} from 'remotion';\nimport {Easing} from 'remotion';\nimport {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';\n\nconst animationDuration = 20;\nconst cameraAltitude = 4000;\n```\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\nconst {delayRender, continueRender} = useDelayRender();\n\nuseEffect(() => {\n  if (!map) {\n    return;\n  }\n  const handle = delayRender('Moving point...');\n\n  const routeDistance = turf.length(turf.lineString(lineCoordinates));\n\n  const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {\n    easing: Easing.inOut(Easing.sin),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const camera = map.getFreeCameraOptions();\n\n  const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;\n\n  camera.lookAtPoint({\n    lng: alongRoute[0],\n    lat: alongRoute[1],\n  });\n\n  map.setFreeCameraOptions(camera);\n  map.once('idle', () => continueRender(handle));\n}, [lineCoordinates, fps, frame, handle, map]);\n```\n\nNotes:\n\nIMPORTANT: Keep the camera by default so north is up.\nIMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.\n\n- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors\n- See [Timing](./timing.md) for more options for timing.\n- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.\n\n## Animating lines\n\n### Straight lines (linear interpolation)\n\nTo animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {durationInFrames} = useVideoConfig();\n\nuseEffect(() => {\n  if (!map) return;\n\n  const animationHandle = delayRender('Animating line...');\n\n  const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  // Linear interpolation for a straight line on the map\n  const start = lineCoordinates[0];\n  const end = lineCoordinates[1];\n  const currentLng = start[0] + (end[0] - start[0]) * progress;\n  const currentLat = start[1] + (end[1] - start[1]) * progress;\n\n  const lineData: GeoJSON.Feature<GeoJSON.LineString> = {\n    type: 'Feature',\n    properties: {},\n    geometry: {\n      type: 'LineString',\n      coordinates: [start, [currentLng, currentLat]],\n    },\n  };\n\n  const source = map.getSource('trace') as mapboxgl.GeoJSONSource;\n  if (source) {\n    source.setData(lineData);\n  }\n\n  map.once('idle', () => continueRender(animationHandle));\n}, [frame, map, durationInFrames]);\n```\n\n### Curved lines (geodesic/great circle)\n\nTo animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.\n\n```tsx\nimport * as turf from '@turf/turf';\n\nconst routeLine = turf.lineString(lineCoordinates);\nconst routeDistance = turf.length(routeLine);\n\nconst currentDistance = Math.max(0.001, routeDistance * progress);\nconst slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);\n\nconst source = map.getSource('route') as mapboxgl.GeoJSONSource;\nif (source) {\n  source.setData(slicedLine);\n}\n```\n\n## Markers\n\nAdd labels, and markers where appropriate.\n\n```tsx\n_map.addSource('markers', {\n  type: 'geojson',\n  data: {\n    type: 'FeatureCollection',\n    features: [\n      {\n        type: 'Feature',\n        properties: {name: 'Point 1'},\n        geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},\n      },\n    ],\n  },\n});\n\n_map.addLayer({\n  id: 'city-markers',\n  type: 'circle',\n  source: 'markers',\n  paint: {\n    'circle-radius': 40,\n    'circle-color': '#FF4444',\n    'circle-stroke-width': 4,\n    'circle-stroke-color': '#FFFFFF',\n  },\n});\n\n_map.addLayer({\n  id: 'labels',\n  type: 'symbol',\n  source: 'markers',\n  layout: {\n    'text-field': ['get', 'name'],\n    'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],\n    'text-size': 50,\n    'text-offset': [0, 0.5],\n    'text-anchor': 'top',\n  },\n  paint: {\n    'text-color': '#FFFFFF',\n    'text-halo-color': '#000000',\n    'text-halo-width': 2,\n  },\n});\n```\n\nMake sure they are big enough. Check the composition dimensions and scale the labels accordingly.\nFor a composition size of 1920x1080, the label font size should be at least 40px.\n\nIMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:\n\n```tsx\n\"text-offset\": [0, 0.5],\n```\n\n## 3D buildings\n\nTo enable 3D buildings, use the following code:\n\n```tsx\n_map.setConfigProperty('basemap', 'show3dObjects', true);\n_map.setConfigProperty('basemap', 'show3dLandmarks', true);\n_map.setConfigProperty('basemap', 'show3dBuildings', true);\n```\n\n## Rendering\n\nWhen rendering a map animation, make sure to render with the following flags:\n\n```\nnpx remotion render --gl=angle --concurrency=1\n```\n","skills/remotion-best-practices/rules/measuring-dom-nodes.md":"---\nname: measuring-dom-nodes\ndescription: Measuring DOM element dimensions in Remotion\nmetadata:\n  tags: measure, layout, dimensions, getBoundingClientRect, scale\n---\n\n# Measuring DOM nodes in Remotion\n\nRemotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.\n\n## Measuring element dimensions\n\n```tsx\nimport { useCurrentScale } from \"remotion\";\nimport { useRef, useEffect, useState } from \"react\";\n\nexport const MyComponent = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const scale = useCurrentScale();\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    if (!ref.current) return;\n    const rect = ref.current.getBoundingClientRect();\n    setDimensions({\n      width: rect.width / scale,\n      height: rect.height / scale,\n    });\n  }, [scale]);\n\n  return <div ref={ref}>Content to measure</div>;\n};\n```\n\n","skills/remotion-best-practices/rules/measuring-text.md":"---\nname: measuring-text\ndescription: Measuring text dimensions, fitting text to containers, and checking overflow\nmetadata:\n  tags: measure, text, layout, dimensions, fitText, fillTextBox\n---\n\n# Measuring text in Remotion\n\n## Prerequisites\n\nInstall @remotion/layout-utils if it is not already installed:\n\n```bash\nnpx remotion add @remotion/layout-utils # If project uses npm\nbunx remotion add @remotion/layout-utils # If project uses bun\nyarn remotion add @remotion/layout-utils # If project uses yarn\npnpm exec remotion add @remotion/layout-utils # If project uses pnpm\n```\n\n## Measuring text dimensions\n\nUse `measureText()` to calculate the width and height of text:\n\n```tsx\nimport { measureText } from \"@remotion/layout-utils\";\n\nconst { width, height } = measureText({\n  text: \"Hello World\",\n  fontFamily: \"Arial\",\n  fontSize: 32,\n  fontWeight: \"bold\",\n});\n```\n\nResults are cached - duplicate calls return the cached result.\n\n## Fitting text to a width\n\nUse `fitText()` to find the optimal font size for a container:\n\n```tsx\nimport { fitText } from \"@remotion/layout-utils\";\n\nconst { fontSize } = fitText({\n  text: \"Hello World\",\n  withinWidth: 600,\n  fontFamily: \"Inter\",\n  fontWeight: \"bold\",\n});\n\nreturn (\n  <div\n    style={{\n      fontSize: Math.min(fontSize, 80), // Cap at 80px\n      fontFamily: \"Inter\",\n      fontWeight: \"bold\",\n    }}\n  >\n    Hello World\n  </div>\n);\n```\n\n## Checking text overflow\n\nUse `fillTextBox()` to check if text exceeds a box:\n\n```tsx\nimport { fillTextBox } from \"@remotion/layout-utils\";\n\nconst box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });\n\nconst words = [\"Hello\", \"World\", \"This\", \"is\", \"a\", \"test\"];\nfor (const word of words) {\n  const { exceedsBox } = box.add({\n    text: word + \" \",\n    fontFamily: \"Arial\",\n    fontSize: 24,\n  });\n  if (exceedsBox) {\n    // Text would overflow, handle accordingly\n    break;\n  }\n}\n```\n\n## Best practices\n\n**Load fonts first:** Only call measurement functions after fonts are loaded.\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\n\nconst { fontFamily, waitUntilDone } = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nwaitUntilDone().then(() => {\n  // Now safe to measure\n  const { width } = measureText({\n    text: \"Hello\",\n    fontFamily,\n    fontSize: 32,\n  });\n})\n```\n\n**Use validateFontIsLoaded:** Catch font loading issues early:\n\n```tsx\nmeasureText({\n  text: \"Hello\",\n  fontFamily: \"MyCustomFont\",\n  fontSize: 32,\n  validateFontIsLoaded: true, // Throws if font not loaded\n});\n```\n\n**Match font properties:** Use the same properties for measurement and rendering:\n\n```tsx\nconst fontStyle = {\n  fontFamily: \"Inter\",\n  fontSize: 32,\n  fontWeight: \"bold\" as const,\n  letterSpacing: \"0.5px\",\n};\n\nconst { width } = measureText({\n  text: \"Hello\",\n  ...fontStyle,\n});\n\nreturn <div style={fontStyle}>Hello</div>;\n```\n\n**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:\n\n```tsx\n<div style={{ outline: \"2px solid red\" }}>Text</div>\n```\n","skills/remotion-best-practices/rules/parameters.md":"---\nname: parameters\ndescription: Make a video parametrizable by adding a Zod schema\nmetadata:\n  tags: parameters, zod, schema\n---\n\nTo make a video parametrizable, a Zod schema can be added to a composition.\n\nFirst, `zod` must be installed - it must be exactly version `3.22.3`.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i zod@3.22.3\n```\n\nIf `bun.lockb` is found, use the following command:\n\n```bash\nbun i zod@3.22.3\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add zod@3.22.3\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i zod@3.22.3\n```\n\nThen, a Zod schema can be defined alongside the component:\n\n```tsx title=\"src/MyComposition.tsx\"\nimport {z} from 'zod';\n\nexport const MyCompositionSchema = z.object({\n  title: z.string(),\n});\n\nconst MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => {\n  return (\n    <div>\n      <h1>{props.title}</h1>\n    </div>\n  );\n};\n```\n\nIn the root file, the schema can be passed to the composition:\n\n```tsx title=\"src/Root.tsx\"\nimport {Composition} from 'remotion';\nimport {MycComponent, MyCompositionSchema} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{title: 'Hello World'}} schema={MyCompositionSchema} />;\n};\n```\n\nNow, the user can edit the parameter visually in the sidebar.\n\nAll schemas that are supported by Zod are supported by Remotion.\n\nRemotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.\n\n## Color picker\n\nFor adding a color picker, use `zColor()` from `@remotion/zod-types`.\n\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/zod-types # If project uses npm\nbunx remotion add @remotion/zod-types # If project uses bun\nyarn remotion add @remotion/zod-types # If project uses yarn\npnpm exec remotion add @remotion/zod-types # If project uses pnpm\n```\n\nThen import `zColor` from `@remotion/zod-types`:\n\n```tsx\nimport {zColor} from '@remotion/zod-types';\n```\n\nThen use it in the schema:\n\n```tsx\nexport const MyCompositionSchema = z.object({\n  color: zColor(),\n});\n```\n","skills/remotion-best-practices/rules/sequencing.md":"---\nname: sequencing\ndescription: Sequencing patterns for Remotion - delay, trim, limit duration of items\nmetadata:\n  tags: sequence, series, timing, delay, trim\n---\n\nUse `<Sequence>` to delay when an element appears in the timeline.\n\n```tsx\nimport { Sequence } from \"remotion\";\n\nconst {fps} = useVideoConfig();\n\n<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Title />\n</Sequence>\n<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Subtitle />\n</Sequence>\n```\n\nThis will by default wrap the component in an absolute fill element.  \nIf the items should not be wrapped, use the `layout` prop:\n\n```tsx\n<Sequence layout=\"none\">\n  <Title />\n</Sequence>\n```\n\n## Premounting\n\nThis loads the component in the timeline before it is actually played.  \nAlways premount any `<Sequence>`!\n\n```tsx\n<Sequence premountFor={1 * fps}>\n  <Title />\n</Sequence>\n```\n\n## Series\n\nUse `<Series>` when elements should play one after another without overlap.\n\n```tsx\nimport {Series} from 'remotion';\n\n<Series>\n  <Series.Sequence durationInFrames={45}>\n    <Intro />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={60}>\n    <MainContent />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={30}>\n    <Outro />\n  </Series.Sequence>\n</Series>;\n```\n\nSame as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.\n\n### Series with overlaps\n\nUse negative offset for overlapping sequences:\n\n```tsx\n<Series>\n  <Series.Sequence durationInFrames={60}>\n    <SceneA />\n  </Series.Sequence>\n  <Series.Sequence offset={-15} durationInFrames={60}>\n    {/* Starts 15 frames before SceneA ends */}\n    <SceneB />\n  </Series.Sequence>\n</Series>\n```\n\n## Frame References Inside Sequences\n\nInside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):\n\n```tsx\n<Sequence from={60} durationInFrames={30}>\n  <MyComponent />\n  {/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}\n</Sequence>\n```\n\n## Nested Sequences\n\nSequences can be nested for complex timing:\n\n```tsx\n<Sequence from={0} durationInFrames={120}>\n  <Background />\n  <Sequence from={15} durationInFrames={90} layout=\"none\">\n    <Title />\n  </Sequence>\n  <Sequence from={45} durationInFrames={60} layout=\"none\">\n    <Subtitle />\n  </Sequence>\n</Sequence>\n```\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/subtitles.md":"---\nname: subtitles\ndescription: subtitles and caption rules\nmetadata:\n  tags: subtitles, captions, remotion, json\n---\n\nAll captions must be processed in JSON. The captions must use the `Caption` type which is the following:\n\n```ts\nimport type { Caption } from \"@remotion/captions\";\n```\n\nThis is the definition:\n\n```ts\ntype Caption = {\n  text: string;\n  startMs: number;\n  endMs: number;\n  timestampMs: number | null;\n  confidence: number | null;\n};\n```\n\n## Generating captions\n\nTo transcribe video and audio files to generate captions, load the [./transcribe-captions.md](./transcribe-captions.md) file for more instructions.\n\n## Displaying captions\n\nTo display captions in your video, load the [./display-captions.md](./display-captions.md) file for more instructions.\n\n## Importing captions\n\nTo import captions from a .srt file, load the [./import-srt-captions.md](./import-srt-captions.md) file for more instructions.\n","skills/remotion-best-practices/rules/tailwind.md":"---\nname: tailwind\ndescription: Using TailwindCSS in Remotion.\nmetadata:\n---\n\nYou can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.\n\nDon't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.  \n\nTailwind must be installed and enabled first in a Remotion project - fetch  https://www.remotion.dev/docs/tailwind using WebFetch for instructions.","skills/remotion-best-practices/rules/text-animations.md":"---\nname: text-animations\ndescription: Typography and text animation patterns for Remotion.\nmetadata:\n  tags: typography, text, typewriter, highlighter ken\n---\n\n## Text animations\n\nBased on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.\n\n## Typewriter Effect\n\nSee [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.\n\nAlways use string slicing for typewriter effects. Never use per-character opacity.\n\n## Word Highlighting\n\nSee [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.\n","skills/remotion-best-practices/rules/timing.md":"---\nname: timing\ndescription: Interpolation curves in Remotion - linear, easing, spring animations\nmetadata:\n  tags: spring, bounce, easing, interpolation\n---\n\nA simple linear interpolation is done using the `interpolate` function.\n\n```ts title=\"Going from 0 to 1 over 100 frames\"\nimport {interpolate} from 'remotion';\n\nconst opacity = interpolate(frame, [0, 100], [0, 1]);\n```\n\nBy default, the values are not clamped, so the value can go outside the range [0, 1].  \nHere is how they can be clamped:\n\n```ts title=\"Going from 0 to 1 over 100 frames with extrapolation\"\nconst opacity = interpolate(frame, [0, 100], [0, 1], {\n  extrapolateRight: 'clamp',\n  extrapolateLeft: 'clamp',\n});\n```\n\n## Spring animations\n\nSpring animations have a more natural motion.  \nThey go from 0 to 1 over time.\n\n```ts title=\"Spring animation from 0 to 1 over 100 frames\"\nimport {spring, useCurrentFrame, useVideoConfig} from 'remotion';\n\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst scale = spring({\n  frame,\n  fps,\n});\n```\n\n### Physical properties\n\nThe default configuration is: `mass: 1, damping: 10, stiffness: 100`.  \nThis leads to the animation having a bit of bounce before it settles.\n\nThe config can be overwritten like this:\n\n```ts\nconst scale = spring({\n  frame,\n  fps,\n  config: {damping: 200},\n});\n```\n\nThe recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.\n\nHere are some common configurations:\n\n```tsx\nconst smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)\nconst snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)\nconst bouncy = {damping: 8}; // Bouncy entrance (playful animations)\nconst heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce\n```\n\n### Delay\n\nThe animation starts immediately by default.  \nUse the `delay` parameter to delay the animation by a number of frames.\n\n```tsx\nconst entrance = spring({\n  frame: frame - ENTRANCE_DELAY,\n  fps,\n  delay: 20,\n});\n```\n\n### Duration\n\nA `spring()` has a natural duration based on the physical properties.  \nTo stretch the animation to a specific duration, use the `durationInFrames` parameter.\n\n```tsx\nconst spring = spring({\n  frame,\n  fps,\n  durationInFrames: 40,\n});\n```\n\n### Combining spring() with interpolate()\n\nMap spring output (0-1) to custom ranges:\n\n```tsx\nconst springProgress = spring({\n  frame,\n  fps,\n});\n\n// Map to rotation\nconst rotation = interpolate(springProgress, [0, 1], [0, 360]);\n\n<div style={{rotate: rotation + 'deg'}} />;\n```\n\n### Adding springs\n\nSprings return just numbers, so math can be performed:\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps, durationInFrames} = useVideoConfig();\n\nconst inAnimation = spring({\n  frame,\n  fps,\n});\nconst outAnimation = spring({\n  frame,\n  fps,\n  durationInFrames: 1 * fps,\n  delay: durationInFrames - 1 * fps,\n});\n\nconst scale = inAnimation - outAnimation;\n```\n\n## Easing\n\nEasing can be added to the `interpolate` function:\n\n```ts\nimport {interpolate, Easing} from 'remotion';\n\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nThe default easing is `Easing.linear`.  \nThere are various other convexities:\n\n- `Easing.in` for starting slow and accelerating\n- `Easing.out` for starting fast and slowing down\n- `Easing.inOut`\n\nand curves (sorted from most linear to most curved):\n\n- `Easing.quad`\n- `Easing.sin`\n- `Easing.exp`\n- `Easing.circle`\n\nConvexities and curves need be combined for an easing function:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nCubic bezier curves are also supported:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n","skills/remotion-best-practices/rules/transcribe-captions.md":"---\nname: transcribe-captions\ndescription: Transcribing audio to generate captions in Remotion\nmetadata:\n  tags: captions, transcribe, whisper, audio, speech-to-text\n---\n\n# Transcribing audio\n\nTo transcribe audio to generate captions in Remotion, you can use the [`transcribe()`](https://www.remotion.dev/docs/install-whisper-cpp/transcribe) function from the [`@remotion/install-whisper-cpp`](https://www.remotion.dev/docs/install-whisper-cpp) package.\n\n## Prerequisites\n\nFirst, the @remotion/install-whisper-cpp package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/install-whisper-cpp\n```\n\n## Transcribing\n\nMake a Node.js script to download Whisper.cpp and a model, and transcribe the audio.\n\n```ts\nimport path from \"path\";\nimport {\n  downloadWhisperModel,\n  installWhisperCpp,\n  transcribe,\n  toCaptions,\n} from \"@remotion/install-whisper-cpp\";\nimport fs from \"fs\";\n\nconst to = path.join(process.cwd(), \"whisper.cpp\");\n\nawait installWhisperCpp({\n  to,\n  version: \"1.5.5\",\n});\n\nawait downloadWhisperModel({\n  model: \"medium.en\",\n  folder: to,\n});\n\n// Convert the audio to a 16KHz wav file first if needed:\n// import {execSync} from 'child_process';\n// execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y');\n\nconst whisperCppOutput = await transcribe({\n  model: \"medium.en\",\n  whisperPath: to,\n  whisperCppVersion: \"1.5.5\",\n  inputPath: \"/path/to/audio123.wav\",\n  tokenLevelTimestamps: true,\n});\n\n// Optional: Apply our recommended postprocessing\nconst { captions } = toCaptions({\n  whisperCppOutput,\n});\n\n// Write it to the public/ folder so it can be fetched from Remotion\nfs.writeFileSync(\"captions123.json\", JSON.stringify(captions, null, 2));\n```\n\nTranscribe each clip individually and create multiple JSON files.\n\nSee [Displaying captions](display-captions.md) for how to display the captions in Remotion.\n","skills/remotion-best-practices/rules/transitions.md":"---\nname: transitions\ndescription: Scene transitions and overlays for Remotion using TransitionSeries.\nmetadata:\n  tags: transitions, overlays, fade, slide, wipe, scenes\n---\n\n## TransitionSeries\n\n`<TransitionSeries>` arranges scenes and supports two ways to enhance the cut point between them:\n\n- **Transitions** (`<TransitionSeries.Transition>`) — crossfade, slide, wipe, etc. between two scenes. Shortens the timeline because both scenes play simultaneously during the transition.\n- **Overlays** (`<TransitionSeries.Overlay>`) — render an effect (e.g. a light leak) on top of the cut point without shortening the timeline.\n\nChildren are absolutely positioned.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/transitions\n```\n\n## Transition example\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Overlay example\n\nAny React component can be used as an overlay. For a ready-made effect, see the **light-leaks** rule.\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={20}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Mixing transitions and overlays\n\nTransitions and overlays can coexist in the same `<TransitionSeries>`, but an overlay cannot be adjacent to a transition or another overlay.\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneC />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Transition props\n\n`<TransitionSeries.Transition>` requires:\n\n- `presentation` — the visual effect (e.g. `fade()`, `slide()`, `wipe()`).\n- `timing` — controls speed and easing (e.g. `linearTiming()`, `springTiming()`).\n\n## Overlay props\n\n`<TransitionSeries.Overlay>` accepts:\n\n- `durationInFrames` — how long the overlay is visible (positive integer).\n- `offset?` — shifts the overlay relative to the cut point center. Positive = later, negative = earlier. Default: `0`.\n\n## Available transition types\n\nImport transitions from their respective modules:\n\n```tsx\nimport { fade } from \"@remotion/transitions/fade\";\nimport { slide } from \"@remotion/transitions/slide\";\nimport { wipe } from \"@remotion/transitions/wipe\";\nimport { flip } from \"@remotion/transitions/flip\";\nimport { clockWipe } from \"@remotion/transitions/clock-wipe\";\n```\n\n## Slide transition with direction\n\n```tsx\nimport { slide } from \"@remotion/transitions/slide\";\n\n<TransitionSeries.Transition\n  presentation={slide({ direction: \"from-left\" })}\n  timing={linearTiming({ durationInFrames: 20 })}\n/>;\n```\n\nDirections: `\"from-left\"`, `\"from-right\"`, `\"from-top\"`, `\"from-bottom\"`\n\n## Timing options\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\n// Linear timing - constant speed\nlinearTiming({ durationInFrames: 20 });\n\n// Spring timing - organic motion\nspringTiming({ config: { damping: 200 }, durationInFrames: 25 });\n```\n\n## Duration calculation\n\nTransitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations. Overlays do **not** affect the total duration.\n\nFor example, with two 60-frame sequences and a 15-frame transition:\n\n- Without transitions: `60 + 60 = 120` frames\n- With transition: `60 + 60 - 15 = 105` frames\n\nAdding an overlay between two other sequences does not change the total.\n\n### Getting the duration of a transition\n\nUse the `getDurationInFrames()` method on the timing object:\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\nconst linearDuration = linearTiming({\n  durationInFrames: 20,\n}).getDurationInFrames({ fps: 30 });\n// Returns 20\n\nconst springDuration = springTiming({\n  config: { damping: 200 },\n}).getDurationInFrames({ fps: 30 });\n// Returns calculated duration based on spring physics\n```\n\nFor `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.\n\n### Calculating total composition duration\n\n```tsx\nimport { linearTiming } from \"@remotion/transitions\";\n\nconst scene1Duration = 60;\nconst scene2Duration = 60;\nconst scene3Duration = 60;\n\nconst timing1 = linearTiming({ durationInFrames: 15 });\nconst timing2 = linearTiming({ durationInFrames: 20 });\n\nconst transition1Duration = timing1.getDurationInFrames({ fps: 30 });\nconst transition2Duration = timing2.getDurationInFrames({ fps: 30 });\n\nconst totalDuration =\n  scene1Duration +\n  scene2Duration +\n  scene3Duration -\n  transition1Duration -\n  transition2Duration;\n// 60 + 60 + 60 - 15 - 20 = 145 frames\n```\n","skills/remotion-best-practices/rules/transparent-videos.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftransparent-videos.md -->","skills/remotion-best-practices/rules/trimming.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftrimming.md -->","skills/remotion-best-practices/rules/videos.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Fvideos.md -->","skills/remotion/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion%2FSKILL.md -->","skills/shadcn-svelte/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn-svelte%2FSKILL.md -->","skills/shadcn/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn%2FSKILL.md -->","skills/skill-creator/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2FSKILL.md -->","skills/skill-creator/references/output-patterns.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Foutput-patterns.md -->","skills/skill-creator/references/workflows.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Fworkflows.md -->","skills/solid/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsolid%2FSKILL.md -->","skills/svelte/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsvelte%2FSKILL.md -->","skills/tanstack-start/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Ftanstack-start%2FSKILL.md -->","skills/vue/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fvue%2FSKILL.md -->","skills/xstate/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fxstate%2FSKILL.md -->","skills/yaml/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fyaml%2FSKILL.md -->","skills/zustand/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fzustand%2FSKILL.md -->"},"files":{".cursor/mcp.json":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=.cursor%2Fmcp.json -->","AGENTS.md":"# AGENTS.md\n\nInstructions for AI coding agents working with this codebase.\n\n## Package Management\n\n**Always check the latest version before installing a package.**\n\nBefore adding or updating any dependency, verify the current latest version on npm:\n\n```bash\nnpm view <package-name> version\n```\n\nOr check multiple packages at once:\n\n```bash\nnpm view ai version\nnpm view @ai-sdk/provider-utils version\nnpm view zod version\n```\n\nThis ensures we don't install outdated versions that may have incompatible types or missing features.\n\n## Code Style\n\n- Do not use emojis in code or UI\n- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`\n- **Web app docs (`apps/web/`):** Never use Markdown table syntax (`| col | col |`). Always use HTML `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`. Markdown tables do not render correctly in the web app. Inside HTML table cells, curly braces must be escaped as JSX expressions (e.g. `<code>{'{ \"$state\": \"/path\" }'}</code>`) because MDX parses `{` as a JSX expression boundary.\n\n## AI SDK / AI Gateway\n\nWhen using the Vercel AI SDK (`ai` package) with AI Gateway, pass the model as a plain string identifier -- do not import a provider constructor:\n\n```ts\nimport { streamText } from \"ai\";\n\nconst result = streamText({\n  model: \"anthropic/claude-haiku-4.5\",\n  prompt: \"...\",\n});\n```\n\nThis requires `AI_GATEWAY_API_KEY` to be set in the environment. See `tests/e2e/` for examples.\n\n## Dev Servers\n\nAll apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.\n\nNaming convention:\n- Main web app: `json-render` → `json-render.localhost:1355`\n- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`\n\nWhen adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless my-example-demo.json-render next dev --turbopack\"\n  }\n}\n```\n\nDo **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.\n\n## Workflow\n\n- Run `pnpm type-check` after each turn to ensure type safety\n- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync.\n- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content.\n- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:\n  - Package `README.md` files in `packages/*/README.md`\n  - Root `README.md` (if packages table, install commands, or examples are affected)\n  - Web app docs in `apps/web/` (if guides, API references, or examples need updating)\n  - Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)\n  - `AGENTS.md` (if workflow or conventions change)\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nAll public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.\n\n### Preparing a release\n\nWhen asked to prepare a release (e.g. \"prepare v0.17.0\"):\n\n1. Create a branch (e.g. `prepare-v0.17.0`)\n2. Bump the version in `packages/core/package.json`\n3. Run `pnpm run version:sync` to update all other `@json-render/*` packages\n4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)\n5. **Fill documentation gaps** — every public package should have:\n   - A row in the root `README.md` packages table\n   - A renderer section in the root `README.md` (if it's a renderer)\n   - An API reference page at `apps/web/content/docs/api/<name>.mdx`\n   - An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`\n   - An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)\n   - A skill at `skills/<name>/SKILL.md`\n   - A `packages/<name>/README.md`\n6. **Run `pnpm type-check`** after all changes to verify nothing is broken\n7. Open a PR and merge to `main`\n\nCI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n### Scripts\n\n- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`\n- `pnpm run version:check` — verify all versions are in sync (runs in CI)\n- `pnpm run ci:publish` — build all packages and publish to npm (CI only)\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","examples/devtools/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=examples%2Fdevtools%2FREADME.md -->","packages/codegen/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcodegen%2FREADME.md -->","packages/core/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcore%2FREADME.md -->","packages/devtools-react/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-react%2FREADME.md -->","packages/devtools-solid/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-solid%2FREADME.md -->","packages/devtools-svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-svelte%2FREADME.md -->","packages/devtools-vue/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-vue%2FREADME.md -->","packages/devtools/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools%2FREADME.md -->","packages/directives/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdirectives%2FREADME.md -->","packages/eslint-config/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Feslint-config%2FREADME.md -->","packages/image/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fimage%2FREADME.md -->","packages/ink/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fink%2FREADME.md -->","packages/jotai/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fjotai%2FREADME.md -->","packages/mcp/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fmcp%2FREADME.md -->","packages/next/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fnext%2FREADME.md -->","packages/react-email/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-email%2FREADME.md -->","packages/react-native/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-native%2FREADME.md -->","packages/react-pdf/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-pdf%2FREADME.md -->","packages/react-three-fiber/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-three-fiber%2FREADME.md -->","packages/react/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact%2FREADME.md -->","packages/redux/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fredux%2FREADME.md -->","packages/remotion/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fremotion%2FREADME.md -->","packages/shadcn-svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn-svelte%2FREADME.md -->","packages/shadcn/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn%2FREADME.md -->","packages/solid/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsolid%2FREADME.md -->","packages/svelte/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsvelte%2FREADME.md -->","packages/tanstack-start/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Ftanstack-start%2FREADME.md -->","packages/vue/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fvue%2FREADME.md -->","packages/xstate/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fxstate%2FREADME.md -->","packages/yaml/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fyaml%2FREADME.md -->","packages/zustand/README.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fzustand%2FREADME.md -->","skills/codegen/SKILL.md":"---\nname: codegen\ndescription: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen.\n---\n\n# @json-render/codegen\n\nFramework-agnostic utilities for generating code from json-render UI trees. Use these to build custom code exporters for Next.js, Remix, or other frameworks.\n\n## Installation\n\n```bash\nnpm install @json-render/codegen\n```\n\n## Tree Traversal\n\n```typescript\nimport {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n} from \"@json-render/codegen\";\n\n// Walk the spec depth-first\ntraverseSpec(spec, (element, key, depth, parent) => {\n  console.log(`${\" \".repeat(depth * 2)}${key}: ${element.type}`);\n});\n\n// Get all component types used\nconst components = collectUsedComponents(spec);\n// Set { \"Card\", \"Metric\", \"Button\" }\n\n// Get all state paths referenced\nconst statePaths = collectStatePaths(spec);\n// Set { \"analytics/revenue\", \"user/name\" }\n\n// Get all action names\nconst actions = collectActions(spec);\n// Set { \"submit_form\", \"refresh_data\" }\n```\n\n## Serialization\n\n```typescript\nimport {\n  serializePropValue,\n  serializeProps,\n  escapeString,\n  type SerializeOptions,\n} from \"@json-render/codegen\";\n\n// Serialize a single value\nserializePropValue(\"hello\");\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue({ $state: \"/user/name\" });\n// { value: '{ $state: \"/user/name\" }', needsBraces: true }\n\n// Serialize props for JSX\nserializeProps({ title: \"Dashboard\", columns: 3, disabled: true });\n// 'title=\"Dashboard\" columns={3} disabled'\n\n// Escape strings for code\nescapeString('hello \"world\"');\n// 'hello \\\"world\\\"'\n```\n\n### SerializeOptions\n\n```typescript\ninterface SerializeOptions {\n  quotes?: \"single\" | \"double\";\n  indent?: number;\n}\n```\n\n## Types\n\n```typescript\nimport type { GeneratedFile, CodeGenerator } from \"@json-render/codegen\";\n\nconst myGenerator: CodeGenerator = {\n  generate(spec) {\n    return [\n      { path: \"package.json\", content: \"...\" },\n      { path: \"app/page.tsx\", content: \"...\" },\n    ];\n  },\n};\n```\n\n## Building a Custom Generator\n\n```typescript\nimport {\n  collectUsedComponents,\n  collectStatePaths,\n  traverseSpec,\n  serializeProps,\n  type GeneratedFile,\n} from \"@json-render/codegen\";\nimport type { Spec } from \"@json-render/core\";\n\nexport function generateNextJSProject(spec: Spec): GeneratedFile[] {\n  const files: GeneratedFile[] = [];\n  const components = collectUsedComponents(spec);\n  // Generate package.json, component files, main page...\n  return files;\n}\n```\n","skills/core/SKILL.md":"---\nname: core\ndescription: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.\n---\n\n# @json-render/core\n\nCore package for schema definition, catalog creation, and spec streaming.\n\n## Key Concepts\n\n- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)\n- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)\n- **Spec**: JSON output from AI that conforms to the schema\n- **SpecStream**: JSONL streaming format for progressive spec building\n\n## Experimental Decision-Model Composition\n\nFor decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.\n\n- Run the Gateway evaluator server-side with `{ model: \"typesafe-ai/jev\", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.\n- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.\n- New trees default to `strategy: \"batch\"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: \"sequential\"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.\n- Batched trace steps use `choice: \"select\" | \"layout\"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.\n- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.\n- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.\n- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.\n- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.\n- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.\n- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.\n- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.\n- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.\n\nSee `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.\n\n## Defining a Schema\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nexport const schema = defineSchema((s) => ({\n  spec: s.object({\n    // Define spec structure\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate, // Optional custom AI prompt\n});\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"./schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button component\",\n    },\n  },\n});\n```\n\n## Generating AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt(); // Uses schema's promptTemplate\nconst systemPrompt = catalog.prompt({ customRules: [\"Rule 1\", \"Rule 2\"] });\n```\n\n## SpecStream Utilities\n\nFor streaming AI responses (JSONL patches):\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process streaming chunks\nconst { result, newPatches } = compiler.push(chunk);\n\n// Get final result\nconst finalSpec = compiler.getResult();\n```\n\n## Form Values in Action Handlers\n\nUse `findFormValue(\"email\", params, state)` to read a direct parameter, a dotted parameter key (such as `\"form.email\"`), a matching flat state key, or a slash-delimited path (such as `\"/form/email\"`) in nested state. Parameter values are literal, so emails and URLs containing dots are preserved. For action bindings that read nested state, use `{ $state: \"/form/email\" }`; the resolver passes that value to the handler. A bare `\"email\"` field name does not search nested `state.form.email`.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads a value from the state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - evaluates a visibility condition and picks a branch\n- **`{ \"$template\": \"Hello, ${/user/name}!\" }`** - interpolates `${/path}` references with state values\n- **`{ \"$computed\": \"fnName\", \"args\": { \"key\": <expression> } }`** - calls a registered function with resolved args\n\n`$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive).\n\nComponents do not use a `statePath` prop for two-way binding. Instead, use `{ \"$bindState\": \"/path\" }` on the natural value prop (e.g. `value`, `checked`, `pressed`).\n\n```json\n{\n  \"color\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"#007AFF\",\n    \"$else\": \"#8E8E93\"\n  },\n  \"label\": { \"$template\": \"Welcome, ${/user/name}!\" },\n  \"fullName\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\n```typescript\nimport { resolvePropValue, resolveElementProps } from \"@json-render/core\";\n\nconst resolved = resolveElementProps(element.props, { stateModel: myState });\n```\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": { \"value\": { \"$bindState\": \"/form/country\" }, \"options\": [\"US\", \"Canada\"] },\n  \"watch\": {\n    \"/form/country\": { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } }\n  },\n  \"children\": []\n}\n```\n\nWatchers only fire on value changes, not on initial render.\n\n## Validation\n\nBuilt-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`.\n\nCross-field validation uses `$state` expressions in args:\n\n```typescript\nimport { check } from \"@json-render/core\";\n\ncheck.required(\"Field is required\");\ncheck.matches(\"/form/password\", \"Passwords must match\");\ncheck.lessThan(\"/form/endDate\", \"Must be before end date\");\ncheck.greaterThan(\"/form/startDate\", \"Must be after start date\");\ncheck.requiredIf(\"/form/enableNotifications\", \"Required when enabled\");\n```\n\n## User Prompt Builder\n\nBuild structured user prompts with optional spec refinement and state context:\n\n```typescript\nimport { buildUserPrompt } from \"@json-render/core\";\n\n// Fresh generation\nbuildUserPrompt({ prompt: \"create a todo app\" });\n\n// Refinement with edit modes (default: patch-only)\nbuildUserPrompt({ prompt: \"add a toggle\", currentSpec: spec, editModes: [\"patch\", \"merge\"] });\n\n// With runtime state\nbuildUserPrompt({ prompt: \"show data\", state: { todos: [] } });\n```\n\nAvailable edit modes: `\"patch\"` (RFC 6902 JSON Patch), `\"merge\"` (RFC 7396 Merge Patch), `\"diff\"` (unified diff).\n\n## Spec Validation\n\nValidate spec structure and auto-fix common issues:\n\n```typescript\nimport { validateSpec, autoFixSpec } from \"@json-render/core\";\n\nconst { valid, issues } = validateSpec(spec);\n// issues include: missing_child, invalid_visible (malformed conditions),\n// repeat_without_children, repeat_item_outside_scope, repeat_state_mismatch\n\nconst { spec: fixed, fixDetails } = autoFixSpec(spec);\n// fixDetails entries are { message, lossy }. Lossless fixes relocate\n// misplaced fields; lossy fixes prune dangling children references.\n// In a repair loop, withhold lossy fixes until retries are exhausted:\nconst attempt = autoFixSpec(spec, { lossy: retriesExhausted });\n```\n\n## Visibility Conditions\n\nControl element visibility with state-based conditions. `VisibilityContext` is `{ stateModel: StateModel }`.\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\n// Syntax\n{ \"$state\": \"/path\" }                    // truthiness\n{ \"$state\": \"/path\", \"not\": true }      // falsy\n{ \"$state\": \"/path\", \"eq\": value }      // equality\n[ cond1, cond2 ]                         // implicit AND\n\n// Helpers\nvisibility.when(\"/path\")                 // { $state: \"/path\" }\nvisibility.unless(\"/path\")               // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)              // { $state: \"/path\", eq: val }\nvisibility.and(cond1, cond2)             // { $and: [cond1, cond2] }\nvisibility.or(cond1, cond2)              // { $or: [cond1, cond2] }\nvisibility.always                        // true\nvisibility.never                         // false\n```\n\n## Built-in Actions in Schema\n\nSchemas can declare `builtInActions` -- actions that are always available at runtime and auto-injected into prompts:\n\n```typescript\nconst schema = defineSchema(builder, {\n  builtInActions: [\n    { name: \"setState\", description: \"Update a value in the state model\" },\n  ],\n});\n```\n\nThese appear in prompts as `[built-in]` and don't require handlers in `defineRegistry`.\n\n## StateStore\n\nThe `StateStore` interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The `createStateStore` factory creates a simple in-memory implementation:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/core\";\n\nconst store = createStateStore({ count: 0 });\n\nstore.get(\"/count\");         // 0\nstore.set(\"/count\", 1);      // updates and notifies subscribers\nstore.update({ \"/a\": 1, \"/b\": 2 }); // batch update\n\nstore.subscribe(() => {\n  console.log(store.getSnapshot()); // { count: 1 }\n});\n```\n\nThe `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`, `getSnapshot()`, `subscribe(listener)`.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineSchema` | Create a new schema |\n| `defineCatalog` | Create a catalog from schema |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `resolvePropValue` | Resolve a single prop expression against data |\n| `resolveElementProps` | Resolve all prop expressions in an element |\n| `buildUserPrompt` | Build user prompts with refinement and state context |\n| `buildEditUserPrompt` | Build user prompt for editing existing specs |\n| `buildEditInstructions` | Generate prompt section for available edit modes |\n| `isNonEmptySpec` | Check if spec has root and at least one element |\n| `deepMergeSpec` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |\n| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |\n| `EditMode` | Type: `\"patch\" \\| \"merge\" \\| \"diff\"` |\n| `validateSpec` | Validate spec structure |\n| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |\n| `createSpecStreamCompiler` | Stream JSONL patches into spec |\n| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |\n| `parseSpecStreamLine` | Parse single JSONL line |\n| `applySpecStreamPatch` | Apply patch to object |\n| `StateStore` | Interface for plugging in external state management |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `check` | TypeScript helpers for creating validation checks |\n| `BuiltInAction` | Type for built-in action definitions (`name` + `description`) |\n| `ActionBinding` | Action binding type (includes `preventDefault` field) |\n","skills/devtools-react/SKILL.md":"---\nname: devtools-react\ndescription: React adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-react in a React json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-react\n\nReact adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-react\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing React json-render provider so it can observe the renderer's state, actions, and streams:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut.\n\n## Imperative controls\n\nUse `useJsonRenderDevtools` from any mounted React descendant:\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nThe hook returns `null` in production or before the component has mounted. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-solid/SKILL.md":"---\nname: devtools-solid\ndescription: SolidJS adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-solid in a SolidJS json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-solid\n\nSolidJS adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing SolidJS json-render provider:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-svelte/SKILL.md":"---\nname: devtools-svelte\ndescription: Svelte 5 adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-svelte in a Svelte json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-svelte\n\nSvelte 5 adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-svelte\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Svelte json-render provider:\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools-vue/SKILL.md":"---\nname: devtools-vue\ndescription: Vue adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-vue in a Vue json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-vue\n\nVue adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-vue\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Vue json-render provider:\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","skills/devtools/SKILL.md":"---\nname: devtools\ndescription: Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include \"add devtools\", \"debug json-render\", \"inspect the spec\", \"why is this element not rendering\", \"see the state at runtime\", or requests to tap streams / capture action logs for `@json-render/devtools`.\n---\n\n# @json-render/devtools\n\nA floating inspector panel for json-render apps. Framework-agnostic core + per-framework adapters (React, Vue, Svelte, Solid).\n\nProduction-safe: the component renders no devtools UI when `process.env.NODE_ENV === \"production\"`, including when a bundler replaces the expression in a browser without a `process` global. Unbundled environments without `process.env` retain development behavior.\n\n## Install\n\nInstall the core package plus the adapter that matches the host app's renderer.\n\n```bash\n# React\nnpm install @json-render/devtools @json-render/devtools-react\n\n# Vue\nnpm install @json-render/devtools @json-render/devtools-vue\n\n# Svelte\nnpm install @json-render/devtools @json-render/devtools-svelte\n\n# Solid\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Drop-in usage\n\nPlace `<JsonRenderDevtools />` anywhere inside the existing `<JSONUIProvider>` (or framework equivalent). No other wiring required.\n\n### React\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\n### Vue\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\n### Svelte\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\n### Solid\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\n## Controls\n\n- Floating toggle appears bottom-right.\n- Hotkey: `Ctrl`/`Cmd` + `Shift` + `J` (configurable via `hotkey` prop).\n- Drawer is resizable; height persists to localStorage.\n\n## Props\n\n- `spec` (`Spec | null`) — current spec.\n- `catalog` (`Catalog | null`) — catalog definition; required for the Catalog panel.\n- `messages` (`UIMessage[]`) — AI SDK `useChat` messages; scanned for spec data parts.\n- `initialOpen` (`boolean`) — start open.\n- `position` (`\"bottom-right\" | \"bottom-left\" | \"right\"`) — dock + toggle corner. `\"bottom-*\"` docks at the bottom; `\"right\"` docks at the right edge full-height (recommended for app-shells that already use `100vh` or fixed bottom bars).\n- `hotkey` (`string | false`) — `\"mod+shift+j\"` by default.\n- `bufferSize` (`number`) — event ring-buffer cap, default 500.\n- `reserveSpace` (`boolean`, default `true`) — when true the panel pushes the host app by applying `padding-bottom` / `padding-right` on `body`. Set to `false` to keep the panel as a pure overlay.\n- `allowDockToggle` (`boolean`, default `true`) — show a toolbar button so the user can flip the panel between bottom-dock and right-dock. User choice persists to `localStorage` and overrides `position` on subsequent mounts. Pass `false` to lock the dock to `position`.\n- `onEvent` (`(DevtoolsEvent) => void`) — optional tap.\n\n## Panels\n\n- **Spec** — element tree rooted at `spec.root`; props/visibility/events/watchers detail; integrated `validateSpec` warnings.\n- **State** — every JSON Pointer path with inline edit via `store.set`.\n- **Actions** — dispatched actions timeline (name, params, result/error, duration).\n- **Stream** — spec patches, text chunks, token usage, lifecycle markers grouped by generation.\n- **Catalog** — components + actions declared in the catalog with prop chips.\n\n## Picker (toolbar)\n\nThe element picker is a toolbar button in the panel header (Chrome-DevTools-style), not a tab. Click it to activate pick mode, then click any rendered element in the page — selection jumps to the Spec tab with that element focused. `Esc` cancels.\n\n## Reserved space & docking\n\nThe panel can dock at the bottom or the right edge, and by default the user can flip between the two with a toolbar button (the choice persists to `localStorage`). Set `allowDockToggle={false}` if the host app only works with one dock — the button is hidden and the dock is locked to `position`.\n\nPick an initial dock that fits your layout:\n\n- **Bottom dock (default)** — works best for docs / marketing / content-flow sites and for app shells built with a `height: 100%` chain (`html { height: 100% }` → `body { height: 100% }` → `.app { height: 100% }`). The panel writes its height to `--jr-devtools-offset-bottom` and applies matching `padding-bottom` to `body`, so non-fixed content naturally makes room.\n- **Right dock** (`position=\"right\"`) — recommended for app-shell layouts that use `100vh` or `position: fixed; bottom: 0`. Right docking sidesteps the bottom edge entirely and writes its width to `--jr-devtools-offset-right` instead.\n\nApps that use `100vh`, `position: fixed`, or `position: sticky` can opt specific elements in with the published CSS custom properties:\n\n```css\n.composer   { bottom: var(--jr-devtools-offset-bottom, 0); }\n.sidebar    { right:  var(--jr-devtools-offset-right,  0); }\n.app-shell  { height: calc(100vh - var(--jr-devtools-offset-bottom, 0)); }\n```\n\nIf the automatic body padding causes problems with a particular layout, pass `reserveSpace={false}` to make the panel a pure overlay — the CSS custom properties are still published so you can reserve space manually.\n\n(`--jr-devtools-offset` is kept as a back-compat alias for whichever edge is currently active.)\n\n## Multiple renderers on one page (e.g. a chat)\n\nA single `<JsonRenderDevtools />` can inspect many `<Renderer />` instances at once — a chat where each assistant message renders its own spec, a dashboard made of several independent widgets, etc. The recipe:\n\n1. **One top-level `<JSONUIProvider>`** so every renderer shares one state store and one action dispatcher. Devtools lives inside this provider and sees everything through it.\n2. **Per-renderer specs, shared state** — each assistant message renders `<Renderer spec={msgSpec} registry={registry} />` directly, not wrapped in its own `StateProvider`. State paths from different messages must not collide.\n3. **Namespace state per turn** — when the source is an AI stream, hand the agent a unique `messageId` and require every element key (`<id>-root`) and state path (`/<id>/count`) to be prefixed with it.\n4. **Pass `spec={latest}` + `messages={all}`** — `spec` drives the Spec panel (usually the newest assistant message's spec), while `messages` feeds the Stream panel with patches from every turn.\n5. **Actions and the picker are already global** — `registerActionObserver` captures dispatches from any `ActionProvider` in the tree, and `data-jr-key` is written by the renderer itself, so Pick works across every rendered element regardless of which message produced it.\n\nSee `examples/devtools` for a full AI chat wired this way.\n\n## Imperative API (React only)\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nReturns `null` in production or before the component mounts.\n\n## Server-side stream tap\n\nCapture spec patches at the API route so events persist server-side or flow into your own telemetry.\n\n```ts\nimport { tapJsonRenderStream, createEventStore } from \"@json-render/devtools\";\nimport { pipeJsonRender } from \"@json-render/core\";\n\nconst events = createEventStore({ bufferSize: 1000 });\nconst tapped = tapJsonRenderStream(result.toUIMessageStream(), events);\nwriter.merge(pipeJsonRender(tapped));\n```\n\nYAML equivalent: `tapYamlStream`.\n\n## Under the hood\n\n- **Shadow-DOM isolated panel** — the panel's styles never leak into the host app and vice versa.\n- **Ring-buffered event store** — capped log of devtools events (state changes, action dispatches, stream patches, etc.).\n- **Action observer registry** — each framework's `ActionProvider` reports via `notifyActionDispatch` / `notifyActionSettle` in `@json-render/core`; devtools subscribes via `registerActionObserver`.\n- **Picker element tagging** — while devtools is mounted, `ElementRenderer` wraps each rendered element in `<span data-jr-key=\"...\" style=\"display:contents\">` so the picker can map DOM → spec key. No layout impact.\n","skills/directives/SKILL.md":"---\nname: directives\ndescription: Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs.\n---\n\n# @json-render/directives\n\nPre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.\n\n## Quick Start\n\n```typescript\nimport { standardDirectives } from '@json-render/directives';\n\n// Wire into prompt generation\nconst prompt = catalog.prompt({ directives: standardDirectives });\n\n// Wire into the renderer (React example)\nimport { JSONUIProvider, Renderer } from '@json-render/react';\n\n<JSONUIProvider registry={registry} directives={standardDirectives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\nTo add factory directives like `createI18nDirective`, spread the array:\n\n```typescript\nimport { standardDirectives, createI18nDirective } from '@json-render/directives';\n\nconst directives = [...standardDirectives, createI18nDirective(config)];\n```\n\n## Defining Custom Directives\n\nUse `defineDirective` from `@json-render/core`:\n\n```typescript\nimport { defineDirective, resolvePropValue } from '@json-render/core';\nimport { z } from 'zod';\n\nconst doubleDirective = defineDirective({\n  name: '$double',\n  description: 'Double a numeric value.',\n  schema: z.object({\n    $double: z.unknown(),\n  }),\n  resolve(value, ctx) {\n    const resolved = resolvePropValue(value.$double, ctx);\n    return (resolved as number) * 2;\n  },\n});\n```\n\nRules:\n- Name must start with `$`\n- Name must not conflict with built-in keys (`$state`, `$cond`, `$computed`, `$template`, `$item`, `$index`, `$bindState`, `$bindItem`)\n- Resolvers should call `resolvePropValue` on sub-values to support composition\n\n## Built-in Directives\n\n### `$format` — Locale-aware value formatting\n\nFormats values using `Intl` formatters. Supports `date`, `currency`, `number`, and `percent`.\n\n```json\n{ \"$format\": \"currency\", \"value\": { \"$state\": \"/cart/total\" }, \"currency\": \"USD\" }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/user/createdAt\" } }\n{ \"$format\": \"number\", \"value\": 1234567, \"notation\": \"compact\" }\n{ \"$format\": \"percent\", \"value\": 0.75 }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/post/createdAt\" }, \"style\": \"relative\" }\n```\n\nFields: `$format` (date | currency | number | percent), `value` (any expression), `locale?` (string), `currency?` (string, default \"USD\"), `notation?` (string), `style?` (\"relative\" for relative dates), `options?` (extra Intl options).\n\n### `$math` — Arithmetic operations\n\n```json\n{ \"$math\": \"add\", \"a\": { \"$state\": \"/subtotal\" }, \"b\": { \"$state\": \"/tax\" } }\n{ \"$math\": \"round\", \"a\": 3.7 }\n```\n\nOperations: `add`, `subtract`, `multiply`, `divide`, `mod`, `min`, `max`, `round`, `floor`, `ceil`, `abs`. Unary ops (`round`, `floor`, `ceil`, `abs`) only use `a`. Division by zero returns `0`.\n\nFields: `$math` (operation enum), `a?` (first operand, default 0), `b?` (second operand, default 0).\n\n### `$concat` — String concatenation\n\n```json\n{ \"$concat\": [{ \"$state\": \"/user/firstName\" }, \" \", { \"$state\": \"/user/lastName\" }] }\n```\n\nFields: `$concat` (array of values to resolve and join into a string).\n\n### `$count` — Array/string length\n\n```json\n{ \"$count\": { \"$state\": \"/cart/items\" } }\n```\n\nReturns `.length` of arrays or strings, `0` for other types.\n\nFields: `$count` (value to count).\n\n### `$truncate` — Text truncation\n\n```json\n{ \"$truncate\": { \"$state\": \"/post/body\" }, \"length\": 140, \"suffix\": \"...\" }\n```\n\nFields: `$truncate` (value to truncate), `length?` (max chars, default 100), `suffix?` (string, default \"...\").\n\n### `$pluralize` — Singular/plural forms\n\n```json\n{ \"$pluralize\": { \"$state\": \"/cart/itemCount\" }, \"one\": \"item\", \"other\": \"items\", \"zero\": \"no items\" }\n```\n\nOutput: `\"3 items\"`, `\"1 item\"`, or `\"no items\"`.\n\nFields: `$pluralize` (count value), `one` (singular label), `other` (plural label), `zero?` (zero label).\n\n### `$join` — Join array elements\n\n```json\n{ \"$join\": { \"$state\": \"/tags\" }, \"separator\": \", \" }\n```\n\nFields: `$join` (array to join), `separator?` (string, default \", \").\n\n### `createI18nDirective` — Internationalization factory\n\n```typescript\nimport { createI18nDirective } from '@json-render/directives';\n\nconst tDirective = createI18nDirective({\n  locale: 'en',\n  messages: {\n    en: { \"greeting\": \"Hello, {{name}}!\", \"checkout.submit\": \"Place Order\" },\n    es: { \"greeting\": \"Hola, {{name}}!\", \"checkout.submit\": \"Realizar Pedido\" },\n  },\n  fallbackLocale: 'en',\n});\n```\n\nUsage in specs:\n\n```json\n{ \"$t\": \"checkout.submit\" }\n{ \"$t\": \"greeting\", \"params\": { \"name\": { \"$state\": \"/user/name\" } } }\n```\n\nFields: `$t` (translation key), `params?` (interpolation parameters, values accept expressions).\n\nConfig: `locale` (current locale), `messages` (Record<locale, Record<key, string>>), `fallbackLocale?` (fallback when key missing).\n\n## Composition\n\nDirectives compose naturally — each resolver calls `resolvePropValue` on its inputs, so directives can wrap other directives or built-in expressions:\n\n```json\n{\n  \"$format\": \"currency\",\n  \"value\": { \"$math\": \"multiply\", \"a\": { \"$state\": \"/price\" }, \"b\": { \"$state\": \"/qty\" } },\n  \"currency\": \"USD\"\n}\n```\n\nResolves inside-out: `$state` reads from state, `$math` multiplies, `$format` formats as currency.\n\n## Wiring into Renderers\n\nAll four renderers (React, Vue, Svelte, Solid) accept `directives` on their provider and `createRenderer` output:\n\n```tsx\n// Provider pattern\n<JSONUIProvider registry={registry} directives={directives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n\n// createRenderer pattern\nconst MyRenderer = createRenderer(catalog, components);\n<MyRenderer spec={spec} directives={directives} />\n```\n\nFor prompt generation, pass the same array:\n\n```typescript\nconst prompt = catalog.prompt({ directives });\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `formatDirective` | `$format` directive definition |\n| `mathDirective` | `$math` directive definition |\n| `concatDirective` | `$concat` directive definition |\n| `countDirective` | `$count` directive definition |\n| `truncateDirective` | `$truncate` directive definition |\n| `pluralizeDirective` | `$pluralize` directive definition |\n| `joinDirective` | `$join` directive definition |\n| `createI18nDirective` | Factory for `$t` i18n directive |\n| `standardDirectives` | Array of all 7 non-factory directives |\n| `I18nConfig` | Type for i18n configuration |\n","skills/image/SKILL.md":"---\nname: image\ndescription: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.\n---\n\n# @json-render/image\n\nImage renderer that converts JSON specs into SVG and PNG images using Satori.\n\n## Quick Start\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"frame\",\n  elements: {\n    frame: {\n      type: \"Frame\",\n      props: { width: 1200, height: 630, backgroundColor: \"#1a1a2e\" },\n      children: [\"heading\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Hello World\", level: \"h1\", color: \"#ffffff\" },\n      children: [],\n    },\n  },\n};\n\nconst png = await renderToPng(spec, {\n  fonts: [{ name: \"Inter\", data: fontData, weight: 400, style: \"normal\" }],\n});\n```\n\n## Using Standard Components\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, standardComponentDefinitions } from \"@json-render/image\";\n\nexport const imageCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Adding Custom Components\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n});\n```\n\n## Standard Components\n\n| Component | Category | Description |\n|-----------|----------|-------------|\n| `Frame` | Root | Root container. Defines width, height, background. Must be root. |\n| `Box` | Layout | Container with padding, margin, border, absolute positioning |\n| `Row` | Layout | Horizontal flex layout |\n| `Column` | Layout | Vertical flex layout |\n| `Heading` | Content | h1-h4 heading text |\n| `Text` | Content | Body text with full styling |\n| `Image` | Content | Image from URL |\n| `Divider` | Decorative | Horizontal line separator |\n| `Spacer` | Decorative | Empty vertical space |\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `renderToSvg` | Render spec to SVG string |\n| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) |\n| `schema` | Image element schema |\n| `standardComponents` | Pre-built component registry |\n| `standardComponentDefinitions` | Catalog definitions for AI prompts |\n\n## Sub-path Exports\n\n| Export | Description |\n|--------|-------------|\n| `@json-render/image` | Full package: schema, components, render functions |\n| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) |\n| `@json-render/image/catalog` | Standard component definitions and types |\n| `@json-render/image/render` | Render functions only |\n","skills/ink/SKILL.md":"---\nname: ink\ndescription: Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.\n---\n\n# @json-render/ink\n\nInk terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/ink/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/ink/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/ink\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    CustomWidget: {\n      props: z.object({ title: z.string() }),\n      slots: [],\n      description: \"Custom widget\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    CustomWidget: ({ props }) => <Text>{props.title}</Text>,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <JSONUIProvider initialState={{}}>\n      <Renderer spec={spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Spec Structure (Flat Element Map)\n\nThe Ink schema uses a flat element map with a root key:\n\n```json\n{\n  \"root\": \"main\",\n  \"elements\": {\n    \"main\": {\n      \"type\": \"Box\",\n      \"props\": { \"flexDirection\": \"column\", \"padding\": 1 },\n      \"children\": [\"heading\", \"content\"]\n    },\n    \"heading\": {\n      \"type\": \"Heading\",\n      \"props\": { \"text\": \"Dashboard\", \"level\": \"h1\" },\n      \"children\": []\n    },\n    \"content\": {\n      \"type\": \"Text\",\n      \"props\": { \"text\": \"Hello from the terminal!\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Standard Components\n\n### Layout\n- `Box` - Flexbox layout container (like a terminal `<div>`). Use for grouping, spacing, borders, alignment. Default flexDirection is row.\n- `Text` - Text output with optional styling (color, bold, italic, etc.)\n- `Newline` - Inserts blank lines. Must be inside a Box with flexDirection column.\n- `Spacer` - Flexible empty space that expands along the main axis.\n\n### Content\n- `Heading` - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)\n- `Divider` - Horizontal separator with optional centered title\n- `Badge` - Colored inline label (variants: default, info, success, warning, error)\n- `Spinner` - Animated loading spinner with optional label\n- `ProgressBar` - Horizontal progress bar (0-1)\n- `Sparkline` - Inline chart using Unicode block characters\n- `BarChart` - Horizontal bar chart with labels and values\n- `Table` - Tabular data with headers and rows\n- `List` - Bulleted or numbered list\n- `ListItem` - Structured list row with title, subtitle, leading/trailing text\n- `Card` - Bordered container with optional title\n- `KeyValue` - Key-value pair display\n- `Link` - Clickable URL with optional label\n- `StatusLine` - Status message with colored icon (info, success, warning, error)\n- `Markdown` - Renders markdown text with terminal styling\n\n### Interactive\n- `TextInput` - Text input field (events: submit, change)\n- `Select` - Selection menu with arrow key navigation (events: change)\n- `MultiSelect` - Multi-selection with space to toggle (events: change, submit)\n- `ConfirmInput` - Yes/No confirmation prompt (events: confirm, deny)\n- `Tabs` - Tab bar navigation with left/right arrow keys (events: change)\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop of form components\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Event System\n\nComponents use `emit` to fire named events. The element's `on` field maps events to action bindings:\n\n```tsx\nCustomButton: ({ props, emit }) => (\n  <Box>\n    <Text>{props.label}</Text>\n    {/* emit(\"press\") triggers the action bound in the spec's on.press */}\n  </Box>\n),\n```\n\n```json\n{\n  \"type\": \"CustomButton\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\n`setState`, `pushState`, and `removeState` are built-in and handled automatically:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n```\n\n## Repeat (Dynamic Lists)\n\nUse the `repeat` field on a container element to render items from a state array:\n\n```json\n{\n  \"type\": \"Box\",\n  \"props\": { \"flexDirection\": \"column\" },\n  \"repeat\": { \"statePath\": \"/items\", \"key\": \"id\" },\n  \"children\": [\"item-row\"]\n}\n```\n\nInside repeated children, use `{ \"$item\": \"field\" }` to read from the current item and `{ \"$index\": true }` for the current index.\n\nFor nested lists, an inner repeat can use `{ \"statePath\": { \"$item\": \"children\" } }` to iterate an array on the enclosing item.\n\n## Streaming\n\nUse `useUIStream` to progressively render specs from JSONL patch streams:\n\n```tsx\nimport { useUIStream } from \"@json-render/ink\";\n\nconst { spec, send, isStreaming } = useUIStream({ api: \"/api/generate\" });\n```\n\n## Server-Side Prompt Generation\n\nUse the `./server` export to generate AI system prompts from your catalog:\n\n```typescript\nimport { catalog } from \"./catalog\";\n\nconst systemPrompt = catalog.prompt({ system: \"You are a terminal assistant.\" });\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched via the event system |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n| `FocusProvider` | Manage focus across interactive components |\n| `JSONUIProvider` | Combined provider for all contexts |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/ink\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## createRenderer (Higher-Level API)\n\n```tsx\nimport { createRenderer } from \"@json-render/ink\";\nimport { standardComponents } from \"@json-render/ink\";\nimport { catalog } from \"./catalog\";\n\nconst InkRenderer = createRenderer(catalog, {\n  ...standardComponents,\n  // custom component overrides here\n});\n\n// InkRenderer includes all providers (state, visibility, actions, focus)\nrender(\n  <InkRenderer spec={spec} state={{ activeTab: \"overview\" }} />\n);\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `createRenderer` | Higher-level: creates a component with built-in providers |\n| `JSONUIProvider` | Combined provider for all contexts |\n| `schema` | Ink flat element map schema (includes built-in state actions) |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way binding for `$bindState`/`$bindItem` expressions |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useOptionalValidation` | Non-throwing variant of useValidation |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n| `Components` | Typed component map (catalog-aware) |\n| `Actions` | Typed action map (catalog-aware) |\n| `ComponentContext` | Typed component context (catalog-aware) |\n| `flatToTree` | Convert flat element map to tree structure |\n\n## Terminal UI Design Guidelines\n\n- Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.\n- Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.\n- Use borderStyle on Box for visual grouping (single, double, round, bold).\n- Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.\n- Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.\n- Use Tabs for multi-view UIs with visible conditions on child content.\n- Use Sparkline for inline trends and BarChart for comparing values.\n","skills/jotai/SKILL.md":"---\nname: jotai\ndescription: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.\n---\n\n# @json-render/jotai\n\nJotai adapter for json-render's `StateStore` interface. Wire a Jotai atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/jotai @json-render/core @json-render/react jotai\n```\n\n## Usage\n\n```tsx\nimport { atom } from \"jotai\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom that holds the json-render state\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = jotaiStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Jotai */}\n</StateProvider>\n```\n\n### With a Shared Jotai Store\n\nWhen your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:\n\n```tsx\nimport { atom, createStore } from \"jotai\";\nimport { Provider as JotaiProvider } from \"jotai/react\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst jStore = createStore();\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\nconst store = jotaiStateStore({ atom: uiAtom, store: jStore });\n\n<JotaiProvider store={jStore}>\n  <StateProvider store={store}>\n    {/* Both json-render and useAtom() see the same state */}\n  </StateProvider>\n</JotaiProvider>\n```\n\n## API\n\n### `jotaiStateStore(options)`\n\nCreates a `StateStore` backed by a Jotai atom.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `WritableAtom<StateModel, [StateModel], void>` | Yes | A writable atom holding the state model |\n| `store` | Jotai `Store` | No | The Jotai store instance. Defaults to a new store. Pass your own to share state with `<Provider>`. |\n","skills/mcp/SKILL.md":"---\nname: mcp\ndescription: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.\n---\n\n# @json-render/mcp\n\nMCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Quick Start\n\n### Server (Node.js)\n\n```typescript\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...shadcnComponentDefinitions },\n  actions: {},\n});\n\nconst server = createMcpApp({\n  name: \"My App\",\n  version: \"1.0.0\",\n  catalog,\n  html: fs.readFileSync(\"dist/index.html\", \"utf-8\"),\n});\n\nawait server.connect(new StdioServerTransport());\n```\n\n### Client (React, inside iframe)\n\n```tsx\nimport { useJsonRenderApp } from \"@json-render/mcp/app\";\nimport { JSONUIProvider, Renderer } from \"@json-render/react\";\n\nfunction McpAppView({ registry }) {\n  const { spec, loading, error } = useJsonRenderApp();\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting...</div>;\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Architecture\n\n1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource\n2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs\n3. The HTML resource is a Vite-bundled single-file React app with json-render renderers\n4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs\n\n## Server API\n\n- `createMcpApp(options)` - main entry, creates a full MCP server\n- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server\n- `registerJsonRenderResource(server, options)` - register the UI resource\n\n## Client API (`@json-render/mcp/app`)\n\n- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`\n- `buildAppHtml(options)` - generate HTML from bundled JS/CSS\n\n## Building the iframe HTML\n\nBundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\n\nexport default defineConfig({\n  plugins: [react(), viteSingleFile()],\n  build: { outDir: \"dist\" },\n});\n```\n\n## Client Configuration\n\n### Cursor (`.cursor/mcp.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## Dependencies\n\n```bash\n# Server\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n\n# Client (iframe)\nnpm install @json-render/react @json-render/shadcn react react-dom\n\n# Build tools\nnpm install -D vite @vitejs/plugin-react vite-plugin-singlefile\n```\n","skills/next/SKILL.md":"---\nname: next\ndescription: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.\n---\n\n# @json-render/next\n\nNext.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.\n\n## Quick Start\n\n```bash\nnpm install @json-render/core @json-render/react @json-render/next\n```\n\n### 1. Define Your Spec\n\n```typescript\n// lib/spec.ts\nimport type { NextAppSpec } from \"@json-render/next\";\n\nexport const spec: NextAppSpec = {\n  metadata: {\n    title: { default: \"My App\", template: \"%s | My App\" },\n    description: \"A json-render Next.js application\",\n  },\n  layouts: {\n    main: {\n      root: \"shell\",\n      elements: {\n        shell: { type: \"Container\", props: {}, children: [\"nav\", \"slot\"] },\n        nav: { type: \"NavBar\", props: { links: [\n          { href: \"/\", label: \"Home\" },\n          { href: \"/about\", label: \"About\" },\n        ]}, children: [] },\n        slot: { type: \"Slot\", props: {}, children: [] },\n      },\n    },\n  },\n  routes: {\n    \"/\": {\n      layout: \"main\",\n      metadata: { title: \"Home\" },\n      page: {\n        root: \"hero\",\n        elements: {\n          hero: { type: \"Card\", props: { title: \"Welcome\" }, children: [] },\n        },\n      },\n    },\n    \"/about\": {\n      layout: \"main\",\n      metadata: { title: \"About\" },\n      page: {\n        root: \"content\",\n        elements: {\n          content: { type: \"Card\", props: { title: \"About Us\" }, children: [] },\n        },\n      },\n    },\n  },\n};\n```\n\n### 2. Create the App\n\n```typescript\n// lib/app.ts\nimport { createNextApp } from \"@json-render/next/server\";\nimport { spec } from \"./spec\";\n\nexport const { Page, generateMetadata, generateStaticParams } = createNextApp({\n  spec,\n  loaders: {\n    // Server-side data loaders (optional)\n    loadPost: async ({ slug }) => {\n      const post = await getPost(slug as string);\n      return { post };\n    },\n  },\n});\n```\n\n### 3. Wire Up Route Files\n\n```tsx\n// app/[[...slug]]/page.tsx\nexport { Page as default, generateMetadata, generateStaticParams } from \"@/lib/app\";\n```\n\n```tsx\n// app/[[...slug]]/layout.tsx\nimport { NextAppProvider } from \"@json-render/next\";\nimport { registry, handlers } from \"@/lib/registry\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <NextAppProvider registry={registry} handlers={handlers}>\n          {children}\n        </NextAppProvider>\n      </body>\n    </html>\n  );\n}\n```\n\n## Key Concepts\n\n### NextAppSpec\n\nThe top-level spec defines an entire Next.js application:\n\n- **metadata**: Root-level SEO metadata (title template, description, OpenGraph)\n- **layouts**: Reusable layout element trees (each must include a `Slot` component)\n- **routes**: Route definitions keyed by URL pattern\n- **state**: Global initial state shared across all routes\n\n### Route Patterns\n\nRoutes use Next.js URL conventions:\n\n- `\"/\"` -- home page\n- `\"/about\"` -- static route\n- `\"/blog/[slug]\"` -- dynamic segment\n- `\"/docs/[...path]\"` -- catch-all segment\n- `\"/settings/[[...path]]\"` -- optional catch-all segment\n\n### Layouts\n\nLayouts wrap page content. Every layout MUST include a `Slot` component where page content will be rendered. Layouts are defined once in `spec.layouts` and referenced by routes via the `layout` field.\n\n### Built-in Components\n\n- **Slot**: Placeholder in layouts where page content is rendered\n- **Link**: Client-side navigation link (wraps `next/link`)\n\n### Built-in Actions\n\n- **setState**: Update state value. Params: `{ statePath, value }`\n- **pushState**: Append to array. Params: `{ statePath, value, clearStatePath? }`\n- **removeState**: Remove from array by index. Params: `{ statePath, index }`\n- **navigate**: Client-side navigation. Params: `{ href }`\n\n### Data Loaders\n\nServer-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.\n\n```typescript\ncreateNextApp({\n  spec,\n  loaders: {\n    loadPost: async ({ slug }) => {\n      const post = await db.post.findUnique({ where: { slug } });\n      return { post };\n    },\n  },\n});\n```\n\n### SSR\n\nPages are server-rendered automatically. The `createNextApp` `Page` component is an async Server Component that:\n\n1. Matches the route from the spec\n2. Runs server-side data loaders\n3. Generates metadata\n4. Passes the resolved spec to the client renderer for hydration\n\n### Entry Points\n\n- `@json-render/next` -- Client components (NextAppProvider, PageRenderer, Link)\n- `@json-render/next/server` -- Server utilities (createNextApp, matchRoute, schema)\n\n## API Reference\n\n### Server Exports (`@json-render/next/server`)\n\n- `createNextApp(options)` -- Create Page, generateMetadata, generateStaticParams\n- `schema` -- Custom schema for Next.js apps (for AI catalog generation)\n- `matchRoute(spec, pathname)` -- Match a URL to a route spec\n- `resolveMetadata(spec, route)` -- Resolve metadata for a route\n- `slugToPath(slug)` -- Convert catch-all slug array to pathname\n- `collectStaticParams(spec)` -- Collect static params for all routes\n\n### Client Exports (`@json-render/next`)\n\n- `NextAppProvider` -- Context provider for registry and handlers\n- `PageRenderer` -- Renders a page spec with optional layout\n- `NextErrorBoundary` -- Error boundary component\n- `NextLoading` -- Loading state component\n- `NextNotFound` -- Not-found component\n- `Link` -- Built-in navigation component (wraps next/link)\n","skills/react-email/SKILL.md":"---\nname: react-email\ndescription: React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email.\nmetadata:\n  tags: react-email, email, json-render, html email, transactional email\n---\n\n# @json-render/react-email\n\nReact Email renderer that converts JSON specs into HTML or plain-text email output.\n\n## Quick Start\n\n```typescript\nimport { renderToHtml } from \"@json-render/react-email\";\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email\";\nimport { defineCatalog } from \"@json-render/core\";\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n\nconst spec = {\n  root: \"html-1\",\n  elements: {\n    \"html-1\": { type: \"Html\", props: { lang: \"en\", dir: \"ltr\" }, children: [\"head-1\", \"body-1\"] },\n    \"head-1\": { type: \"Head\", props: {}, children: [] },\n    \"body-1\": {\n      type: \"Body\",\n      props: { style: { backgroundColor: \"#f6f9fc\" } },\n      children: [\"container-1\"],\n    },\n    \"container-1\": {\n      type: \"Container\",\n      props: { style: { maxWidth: \"600px\", margin: \"0 auto\", padding: \"20px\" } },\n      children: [\"heading-1\", \"text-1\"],\n    },\n    \"heading-1\": { type: \"Heading\", props: { text: \"Welcome\" }, children: [] },\n    \"text-1\": { type: \"Text\", props: { text: \"Thanks for signing up.\" }, children: [] },\n  },\n};\n\nconst html = await renderToHtml(spec);\n```\n\n## Spec Structure (Element Tree)\n\nSame flat element tree as `@json-render/react`: `root` key plus `elements` map. Root must be `Html`; children of `Html` should be `Head` and `Body`. Use `Container` (e.g. max-width 600px) inside `Body` for client-safe layout.\n\n## Creating a Catalog and Registry\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToHtml } from \"@json-render/react-email\";\nimport { standardComponentDefinitions } from \"@json-render/react-email/catalog\";\nimport { Container, Heading, Text } from \"@react-email/components\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Alert: {\n      props: z.object({\n        message: z.string(),\n        variant: z.enum([\"info\", \"success\", \"warning\"]).nullable(),\n      }),\n      slots: [],\n      description: \"A highlighted message block\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Alert: ({ props }) => (\n      <Container style={{ padding: 16, backgroundColor: \"#eff6ff\", borderRadius: 8 }}>\n        <Text style={{ margin: 0 }}>{props.message}</Text>\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\n## Server-Side Render APIs\n\n| Function | Purpose |\n|----------|---------|\n| `renderToHtml(spec, options?)` | Render spec to HTML email string |\n| `renderToPlainText(spec, options?)` | Render spec to plain-text email string |\n\n`RenderOptions`: `registry`, `includeStandard` (default true), `state` (for `$state` / `$cond`).\n\n## Visibility and State\n\nSupports `visible` conditions, `$state`, `$cond`, repeat (`repeat.statePath`), nested repeat paths with `{ \"$item\": \"field\" }`, and the same expression syntax as `@json-render/react`. Use `state` in `RenderOptions` when rendering server-side so expressions resolve.\n\n## Server-Safe Import\n\nImport schema and catalog without React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email/server\";\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create type-safe component registry from catalog |\n| `Renderer` | Render spec in browser (e.g. preview); use with `JSONUIProvider` for state/actions |\n| `createRenderer` | Standalone renderer component with state/actions/validation |\n| `renderToHtml` | Server: spec to HTML string |\n| `renderToPlainText` | Server: spec to plain-text string |\n| `schema` | Email element schema |\n| `standardComponents` | Pre-built component implementations |\n| `standardComponentDefinitions` | Catalog definitions (Zod props) |\n\n## Sub-path Exports\n\n| Path | Purpose |\n|------|---------|\n| `@json-render/react-email` | Full package |\n| `@json-render/react-email/server` | Schema and catalog only (no React) |\n| `@json-render/react-email/catalog` | Standard component definitions and types |\n| `@json-render/react-email/render` | Render functions only |\n\n## Standard Components\n\nAll components accept a `style` prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.\n\n### Document structure\n\n| Component | Description |\n|-----------|-------------|\n| `Html` | Root wrapper (lang, dir). Children: Head, Body. |\n| `Head` | Email head section. |\n| `Body` | Body wrapper; use `style` for background. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Constrain width (e.g. max-width 600px). |\n| `Section` | Group content; table-based for compatibility. |\n| `Row` | Horizontal row. |\n| `Column` | Column in a Row; set width via style. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (as: h1–h6). |\n| `Text` | Body text. |\n| `Link` | Hyperlink (text, href). |\n| `Button` | CTA link styled as button (text, href). |\n| `Image` | Image from URL (src, alt, width, height). |\n| `Hr` | Horizontal rule. |\n\n### Utility\n\n| Component | Description |\n|-----------|-------------|\n| `Preview` | Inbox preview text (inside Html). |\n| `Markdown` | Markdown content as email-safe HTML. |\n\n## Email Best Practices\n\n- Keep width constrained (e.g. Container max-width 600px).\n- Use inline styles or React Email's style props; many clients strip `<style>` blocks.\n- Prefer table-based layout (Section, Row, Column) for broad client support.\n- Use absolute URLs for images; many clients block relative or cid: references in some contexts.\n- Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.\n","skills/react-native/SKILL.md":"---\nname: react-native\ndescription: React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile.\n---\n\n# @json-render/react-native\n\nReact Native renderer that converts JSON specs into native mobile component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-native/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/react-native/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/react-native\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Icon: {\n      props: z.object({ name: z.string(), size: z.number().nullable(), color: z.string().nullable() }),\n      slots: [],\n      description: \"Icon display\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{}}>\n          <Renderer spec={spec} registry={registry} />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Standard Components\n\n### Layout\n- `Container` - wrapper with padding, background, border radius\n- `Row` - horizontal flex layout with gap, alignment\n- `Column` - vertical flex layout with gap, alignment\n- `ScrollContainer` - scrollable area (vertical or horizontal)\n- `SafeArea` - safe area insets for notch/home indicator\n- `Pressable` - touchable wrapper that triggers actions on press\n- `Spacer` - fixed or flexible spacing\n- `Divider` - thin line separator\n\n### Content\n- `Heading` - heading text (levels 1-6)\n- `Paragraph` - body text\n- `Label` - small label text\n- `Image` - image display with sizing modes\n- `Avatar` - circular avatar image\n- `Badge` - small status badge\n- `Chip` - tag/chip for categories\n\n### Input\n- `Button` - pressable button with variants\n- `TextInput` - text input field\n- `Switch` - toggle switch\n- `Checkbox` - checkbox with label\n- `Slider` - range slider\n- `SearchBar` - search input\n\n### Feedback\n- `Spinner` - loading indicator\n- `ProgressBar` - progress indicator\n\n### Composite\n- `Card` - card container with optional header\n- `ListItem` - list row with title, subtitle, accessory\n- `Modal` - bottom sheet modal\n\n## Visibility Conditions\n\nUse `visible` on elements. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `[ cond1, cond2 ]` for AND.\n\n## Pressable + setState Pattern\n\nUse `Pressable` with the built-in `setState` action for interactive UIs like tab bars:\n\n```json\n{\n  \"type\": \"Pressable\",\n  \"props\": {\n    \"action\": \"setState\",\n    \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n  },\n  \"children\": [\"home-icon\", \"home-label\"]\n}\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- Nested lists use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" } } }` inside an enclosing repeat.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n\n```json\n{\n  \"type\": \"TextInput\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Built-in Actions\n\nThe `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:\n\n```json\n{ \"action\": \"setState\", \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched from components |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `schema` | React Native element tree schema |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `createStandardActionHandlers` | Create handlers for standard actions |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way state binding via `$bindState`/`$bindItem` |\n| `useStateBinding` | _(deprecated)_ Legacy two-way binding by path |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n","skills/react-pdf/SKILL.md":"---\nname: react-pdf\ndescription: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files.\n---\n\n# @json-render/react-pdf\n\nReact PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\n## Quick Start\n\n```typescript\nimport { renderToBuffer } from \"@json-render/react-pdf\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"doc\",\n  elements: {\n    doc: { type: \"Document\", props: { title: \"Invoice\" }, children: [\"page\"] },\n    page: {\n      type: \"Page\",\n      props: { size: \"A4\" },\n      children: [\"heading\", \"table\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoice #1234\", level: \"h1\" },\n      children: [],\n    },\n    table: {\n      type: \"Table\",\n      props: {\n        columns: [\n          { header: \"Item\", width: \"60%\" },\n          { header: \"Price\", width: \"40%\", align: \"right\" },\n        ],\n        rows: [\n          [\"Widget A\", \"$10.00\"],\n          [\"Widget B\", \"$25.00\"],\n        ],\n      },\n      children: [],\n    },\n  },\n};\n\nconst buffer = await renderToBuffer(spec);\n```\n\n## Render APIs\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from \"@json-render/react-pdf\";\n\n// In-memory buffer\nconst buffer = await renderToBuffer(spec);\n\n// Readable stream (pipe to HTTP response)\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\n// Direct to file\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nAll render functions accept an optional second argument: `{ registry?, state?, handlers? }`.\n\n## Standard Components\n\n| Component | Description |\n|-----------|-------------|\n| `Document` | Top-level PDF wrapper (must be root) |\n| `Page` | Page with size (A4, LETTER), orientation, margins |\n| `View` | Generic container (padding, margin, background, border) |\n| `Row`, `Column` | Flex layout with gap, align, justify |\n| `Heading` | h1-h4 heading text |\n| `Text` | Body text (fontSize, color, weight, alignment) |\n| `Image` | Image from URL or base64 |\n| `Link` | Hyperlink with text and href |\n| `Table` | Data table with typed columns and rows |\n| `List` | Ordered or unordered list |\n| `Divider` | Horizontal line separator |\n| `Spacer` | Empty vertical space |\n| `PageNumber` | Current page number and total pages |\n\n## Custom Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToBuffer } from \"@json-render/react-pdf\";\nimport { standardComponentDefinitions } from \"@json-render/react-pdf/catalog\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Badge: ({ props }) => (\n      <View style={{ backgroundColor: props.color ?? \"#e5e7eb\", padding: 4 }}>\n        <Text>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## External Store (Controlled Mode)\n\nPass a `StateStore` for full control over state:\n\nNested lists can set `repeat.statePath` to `{ \"$item\": \"field\" }` inside an enclosing repeat.\n\n```typescript\nimport { createStateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\nstore.set(\"/invoice/total\", 200);\n```\n\n## Server-Safe Import\n\nImport schema and catalog without pulling in React:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-pdf/server\";\n```\n","skills/react-three-fiber/SKILL.md":"---\nname: react-three-fiber\ndescription: React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs.\n---\n\n# @json-render/react-three-fiber\n\nReact Three Fiber renderer for json-render. 19 built-in 3D components.\n\n## Two Entry Points\n\n| Entry Point | Exports | Use For |\n|-------------|---------|---------|\n| `@json-render/react-three-fiber/catalog` | `threeComponentDefinitions` | Catalog schemas (no R3F dependency, safe for server) |\n| `@json-render/react-three-fiber` | `threeComponents`, `ThreeRenderer`, `ThreeCanvas`, schemas | R3F implementations and renderer |\n\n## Usage Pattern\n\nPick the 3D components you need from the standard definitions:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { threeComponentDefinitions } from \"@json-render/react-three-fiber/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { threeComponents, ThreeCanvas } from \"@json-render/react-three-fiber\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Box: threeComponentDefinitions.Box,\n    Sphere: threeComponentDefinitions.Sphere,\n    AmbientLight: threeComponentDefinitions.AmbientLight,\n    DirectionalLight: threeComponentDefinitions.DirectionalLight,\n    OrbitControls: threeComponentDefinitions.OrbitControls,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Box: threeComponents.Box,\n    Sphere: threeComponents.Sphere,\n    AmbientLight: threeComponents.AmbientLight,\n    DirectionalLight: threeComponents.DirectionalLight,\n    OrbitControls: threeComponents.OrbitControls,\n  },\n});\n```\n\n## Rendering\n\n### ThreeCanvas (convenience wrapper)\n\n```tsx\n<ThreeCanvas\n  spec={spec}\n  registry={registry}\n  shadows\n  camera={{ position: [5, 5, 5], fov: 50 }}\n  style={{ width: \"100%\", height: \"100vh\" }}\n/>\n```\n\n### Manual Canvas setup\n\n```tsx\nimport { Canvas } from \"@react-three/fiber\";\nimport { ThreeRenderer } from \"@json-render/react-three-fiber\";\n\n<Canvas shadows>\n  <ThreeRenderer spec={spec} registry={registry}>\n    {/* Additional R3F elements */}\n  </ThreeRenderer>\n</Canvas>\n```\n\n## Available Components (19)\n\n### Primitives (7)\n- `Box` -- width, height, depth, material\n- `Sphere` -- radius, widthSegments, heightSegments, material\n- `Cylinder` -- radiusTop, radiusBottom, height, material\n- `Cone` -- radius, height, material\n- `Torus` -- radius, tube, material\n- `Plane` -- width, height, material\n- `Capsule` -- radius, length, material\n\nAll primitives share: `position`, `rotation`, `scale`, `castShadow`, `receiveShadow`, `material`.\n\n### Lights (4)\n- `AmbientLight` -- color, intensity\n- `DirectionalLight` -- position, color, intensity, castShadow\n- `PointLight` -- position, color, intensity, distance, decay\n- `SpotLight` -- position, color, intensity, angle, penumbra\n\n### Other (8)\n- `Group` -- container with position/rotation/scale, supports children\n- `Model` -- GLTF/GLB loader via url prop\n- `Environment` -- HDRI environment map (preset, background, blur, intensity)\n- `Fog` -- linear fog (color, near, far)\n- `GridHelper` -- reference grid (size, divisions, color)\n- `Text3D` -- SDF text (text, fontSize, color, anchorX, anchorY)\n- `PerspectiveCamera` -- camera (position, fov, near, far, makeDefault)\n- `OrbitControls` -- orbit controls (enableDamping, enableZoom, autoRotate)\n\n## Shared Schemas\n\nReusable Zod schemas for custom 3D catalog definitions:\n\n```typescript\nimport { vector3Schema, materialSchema, transformProps, shadowProps } from \"@json-render/react-three-fiber\";\nimport { z } from \"zod\";\n\n// Custom 3D component\nconst myComponentDef = {\n  props: z.object({\n    ...transformProps,\n    ...shadowProps,\n    material: materialSchema.nullable(),\n    myCustomProp: z.string(),\n  }),\n  description: \"My custom 3D component\",\n};\n```\n\n## Material Schema\n\n```typescript\nmaterialSchema = z.object({\n  color: z.string().nullable(),         // default \"#ffffff\"\n  metalness: z.number().nullable(),     // default 0\n  roughness: z.number().nullable(),     // default 1\n  emissive: z.string().nullable(),      // default \"#000000\"\n  emissiveIntensity: z.number().nullable(), // default 1\n  opacity: z.number().nullable(),       // default 1\n  transparent: z.boolean().nullable(),  // default false\n  wireframe: z.boolean().nullable(),    // default false\n});\n```\n\n## Spec Format\n\n3D specs use the standard json-render flat element format:\n\n```json\n{\n  \"root\": \"scene\",\n  \"elements\": {\n    \"scene\": {\n      \"type\": \"Group\",\n      \"props\": { \"position\": [0, 0, 0] },\n      \"children\": [\"light\", \"box\"]\n    },\n    \"light\": {\n      \"type\": \"AmbientLight\",\n      \"props\": { \"intensity\": 0.5 },\n      \"children\": []\n    },\n    \"box\": {\n      \"type\": \"Box\",\n      \"props\": {\n        \"position\": [0, 0.5, 0],\n        \"material\": { \"color\": \"#4488ff\", \"metalness\": 0.3, \"roughness\": 0.7 }\n      },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Dependencies\n\nPeer dependencies required:\n- `@react-three/fiber` >= 8.0.0\n- `@react-three/drei` >= 9.0.0\n- `three` >= 0.160.0\n- `react` ^19.0.0\n- `zod` ^4.0.0\n","skills/react/SKILL.md":"---\nname: react\ndescription: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.\n---\n\n# @json-render/react\n\nReact renderer that converts JSON specs into React component trees.\n\n## Quick Start\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => <div>{props.title}{children}</div>,\n  },\n});\n\nfunction App({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { z } from \"zod\";\n\n// Create catalog with props schemas\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n      description: \"Card container with title\",\n    },\n    Layout: {\n      props: z.object({}),\n      slots: [\"default\", \"header\", \"footer\"],\n      description: \"Layout with named content regions\",\n    },\n  },\n});\n\n// Define component implementations with type-safe props\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Button: ({ props }) => (\n      <button className={props.variant}>{props.label}</button>\n    ),\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h2>{props.title}</h2>\n        {children}\n      </div>\n    ),\n    Layout: ({ children, slots }) => (\n      <div>\n        <header>{slots?.header}</header>\n        <main>{children}</main>\n        <footer>{slots?.footer}</footer>\n      </div>\n    ),\n  },\n});\n```\n\n## Spec Structure (Element Tree)\n\nThe React schema uses an element tree format:\n\n```json\n{\n  \"root\": {\n    \"type\": \"Card\",\n    \"props\": { \"title\": \"Hello\" },\n    \"children\": [{ \"type\": \"Button\", \"props\": { \"label\": \"Click me\" } }]\n  }\n}\n```\n\n## Named Slots\n\nUse `children` for the `\"default\"` slot. Use the element's top-level `slots` object for other slot names declared by the catalog:\n\n```json\n{\n  \"type\": \"Layout\",\n  \"props\": {},\n  \"children\": [\"main\"],\n  \"slots\": {\n    \"header\": [\"heading\"],\n    \"footer\": [\"actions\"]\n  }\n}\n```\n\nRegistry components receive named content as `slots?.header`, `slots?.footer`, and so on. Do not use `slots.default`.\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. New syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR. Helpers: `visibility.when(\"/path\")`, `visibility.unless(\"/path\")`, `visibility.eq(\"/path\", val)`, `visibility.and(cond1, cond2)`, `visibility.or(cond1, cond2)`.\n\n## Providers\n\n| Provider             | Purpose                                                                                                |\n| -------------------- | ------------------------------------------------------------------------------------------------------ |\n| `StateProvider`      | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider`     | Handle actions dispatched via the event system                                                         |\n| `VisibilityProvider` | Enable conditional rendering based on state                                                            |\n| `ValidationProvider` | Form field validation                                                                                  |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>;\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved by the renderer before components receive props:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **Filtered lists**: `repeat` plus an `$item` visible condition on the same container renders only matching items: `{ \"repeat\": { \"statePath\": \"/tasks\", \"key\": \"id\" }, \"visible\": { \"$item\": \"status\", \"eq\": \"todo\" }, \"children\": [\"task-card\"] }`. AND-composed `$state` conjuncts gate the container shell; `$item`/`$index` conjuncts filter items.\n- **Nested lists**: inside a repeat, use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" }, \"key\": \"id\" } }` to iterate an array on the enclosing item.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n- **`{ \"$computed\": \"fn\", \"args\": { ... } }`** - calls registered functions with resolved args\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\nComponents receive already-resolved props. For two-way bound props, use the `useBoundProp` hook with the `bindings` map the renderer provides.\n\nRegister `$computed` functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:\n\n```tsx\n<JSONUIProvider\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n```\n\n## Event System\n\nComponents use `emit` to fire named events, or `on()` to get an event handle with metadata. The element's `on` field maps events to action bindings:\n\n```tsx\n// Simple event firing\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n),\n\n// Event handle with metadata (e.g. preventDefault)\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a href={props.href} onClick={(e) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    }}>{props.label}</a>\n  );\n},\n```\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } }\n}\n```\n\nThe `EventHandle` returned by `on()` has: `emit()`, `shouldPreventDefault` (boolean), and `bound` (boolean).\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\"]\n  },\n  \"watch\": { \"/form/country\": { \"action\": \"loadCities\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\nThe `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in catalog `actions`:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n{ \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } }\n```\n\n`validateForm` validates all registered fields and writes `{ valid, errors }` to state.\n\nNote: `statePath` in action params (e.g. `setState.statePath`) targets the mutation path. Two-way binding in component props uses `{ \"$bindState\": \"/path\" }` on the value prop, not `statePath`.\n\n## useBoundProp\n\nFor form components that need two-way binding, use `useBoundProp` with the `bindings` map the renderer provides when a prop uses `{ \"$bindState\": \"/path\" }` or `{ \"$bindItem\": \"field\" }`:\n\n```tsx\nimport { useBoundProp } from \"@json-render/react\";\n\nInput: ({ element, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(\n    element.props.value,\n    bindings?.value\n  );\n  return (\n    <input\n      value={value ?? \"\"}\n      onChange={(e) => setValue(e.target.value)}\n    />\n  );\n},\n```\n\n`useBoundProp(propValue, bindingPath)` returns `[value, setValue]`. The `value` is the resolved prop; `setValue` writes back to the bound state path (no-op if not bound).\n\n## BaseComponentProps\n\nFor building reusable component libraries not tied to a specific catalog (e.g. `@json-render/shadcn`):\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/react\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (\n  <div>{props.title}{children}</div>\n);\n```\n\n## defineRegistry\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it.\n\n## Key Exports\n\n| Export                  | Purpose                                                                                               |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `defineRegistry`        | Create a type-safe component registry from a catalog                                                  |\n| `Renderer`              | Render a spec using a registry                                                                        |\n| `schema`                | Element tree schema (includes built-in state actions: setState, pushState, removeState, validateForm) |\n| `useStateStore`         | Access state context                                                                                  |\n| `useStateValue`         | Get single value from state                                                                           |\n| `useBoundProp`          | Two-way binding for `$bindState`/`$bindItem` expressions                                              |\n| `useActions`            | Access actions context                                                                                |\n| `useAction`             | Get a single action dispatch function                                                                 |\n| `useOptionalValidation` | Non-throwing variant of useValidation (returns null if no provider)                                   |\n| `useUIStream`           | Stream specs from an API endpoint                                                                     |\n| `createStateStore`      | Create a framework-agnostic in-memory `StateStore`                                                    |\n| `StateStore`            | Interface for plugging in external state management                                                   |\n| `BaseComponentProps`    | Catalog-agnostic base type for reusable component libraries                                           |\n| `EventHandle`           | Event handle type (`emit`, `shouldPreventDefault`, `bound`)                                           |\n| `ComponentContext`      | Typed component context (catalog-aware)                                                               |\n","skills/redux/SKILL.md":"---\nname: redux\ndescription: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux.\n---\n\n# @json-render/redux\n\nRedux adapter for json-render's `StateStore` interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/redux @json-render/core @json-render/react redux\n# or with Redux Toolkit (recommended):\nnpm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit\n```\n\n## Usage\n\n```tsx\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Define a slice for json-render state\nconst uiSlice = createSlice({\n  name: \"ui\",\n  initialState: { count: 0 } as Record<string, unknown>,\n  reducers: {\n    replaceUiState: (_state, action) => action.payload,\n  },\n});\n\n// 2. Create the Redux store\nconst reduxStore = configureStore({\n  reducer: { ui: uiSlice.reducer },\n});\n\n// 3. Create the json-render StateStore adapter\nconst store = reduxStateStore({\n  store: reduxStore,\n  selector: (state) => state.ui,\n  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n});\n\n// 4. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Redux */}\n</StateProvider>\n```\n\n## API\n\n### `reduxStateStore(options)`\n\nCreates a `StateStore` backed by a Redux store.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `Store` | Yes | The Redux store instance |\n| `selector` | `(state) => StateModel` | Yes | Select the json-render slice from the Redux state tree. Use `(s) => s` if the entire state is the model. |\n| `dispatch` | `(nextState, store) => void` | Yes | Dispatch an action that replaces the selected slice with the next state |\n\nThe `dispatch` callback receives the full next state model and the Redux store.\n","skills/remotion-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2FSKILL.md -->","skills/remotion-best-practices/rules/3d.md":"---\nname: 3d\ndescription: 3D content in Remotion using Three.js and React Three Fiber.\nmetadata:\n  tags: 3d, three, threejs\n---\n\n# Using Three.js and React Three Fiber in Remotion\n\nFollow React Three Fiber and Three.js best practices.  \nOnly the following Remotion-specific rules need to be followed:\n\n## Prerequisites\n\nFirst, the `@remotion/three` package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/three # If project uses npm\nbunx remotion add @remotion/three # If project uses bun\nyarn remotion add @remotion/three # If project uses yarn\npnpm exec remotion add @remotion/three # If project uses pnpm\n```\n\n## Using ThreeCanvas\n\nYou MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.  \n`<ThreeCanvas>` MUST have a `width` and `height` prop.\n\n```tsx\nimport { ThreeCanvas } from \"@remotion/three\";\nimport { useVideoConfig } from \"remotion\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <ambientLight intensity={0.4} />\n  <directionalLight position={[5, 5, 5]} intensity={0.8} />\n  <mesh>\n    <sphereGeometry args={[1, 32, 32]} />\n    <meshStandardMaterial color=\"red\" />\n  </mesh>\n</ThreeCanvas>\n```\n\n## No animations not driven by `useCurrentFrame()`\n\nShaders, models etc MUST NOT animate by themselves.  \nNo animations are allowed unless they are driven by `useCurrentFrame()`.  \nOtherwise, it will cause flickering during rendering.  \n\nUsing `useFrame()` from `@react-three/fiber` is forbidden.\n\n## Animate using `useCurrentFrame()`\n\nUse `useCurrentFrame()` to perform animations.\n\n```tsx\nconst frame = useCurrentFrame();\nconst rotationY = frame * 0.02;\n\n<mesh rotation={[0, rotationY, 0]}>\n  <boxGeometry args={[2, 2, 2]} />\n  <meshStandardMaterial color=\"#4a9eff\" />\n</mesh>\n```\n\n## Using `<Sequence>` inside `<ThreeCanvas>`\n\nThe `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.\n\n```tsx\nimport { Sequence } from \"remotion\";\nimport { ThreeCanvas } from \"@remotion/three\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <Sequence layout=\"none\">\n    <mesh>\n      <boxGeometry args={[2, 2, 2]} />\n      <meshStandardMaterial color=\"#4a9eff\" />\n    </mesh>\n  </Sequence>\n</ThreeCanvas>\n```","skills/remotion-best-practices/rules/animations.md":"---\nname: animations\ndescription: Fundamental animation skills for Remotion\nmetadata:\n  tags: animations, transitions, frames, useCurrentFrame\n---\n\nAll animations MUST be driven by the `useCurrentFrame()` hook.  \nWrite animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.\n\n```tsx\nimport { useCurrentFrame } from \"remotion\";\n\nexport const FadeIn = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {\n    extrapolateRight: 'clamp',\n  });\n \n  return (\n    <div style={{ opacity }}>Hello World!</div>\n  );\n};\n```\n\nCSS transitions or animations are FORBIDDEN - they will not render correctly.  \nTailwind animation class names are FORBIDDEN - they will not render correctly.  ","skills/remotion-best-practices/rules/assets.md":"---\nname: assets\ndescription: Importing images, videos, audio, and fonts into Remotion\nmetadata:\n  tags: assets, staticFile, images, fonts, public\n---\n\n# Importing assets in Remotion\n\n## The public folder\n\nPlace assets in the `public/` folder at your project root.\n\n## Using staticFile()\n\nYou MUST use `staticFile()` to reference files from the `public/` folder:\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\nexport const MyComposition = () => {\n  return <Img src={staticFile('logo.png')} />;\n};\n```\n\nThe function returns an encoded URL that works correctly when deploying to subdirectories.\n\n## Using with components\n\n**Images:**\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\n<Img src={staticFile('photo.png')} />;\n```\n\n**Videos:**\n\n```tsx\nimport {Video} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Video src={staticFile('clip.mp4')} />;\n```\n\n**Audio:**\n\n```tsx\nimport {Audio} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Audio src={staticFile('music.mp3')} />;\n```\n\n**Fonts:**\n\n```tsx\nimport {staticFile} from 'remotion';\n\nconst fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);\nawait fontFamily.load();\ndocument.fonts.add(fontFamily);\n```\n\n## Remote URLs\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n<Video src=\"https://remotion.media/video.mp4\" />\n```\n\n## Important notes\n\n- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering\n- Special characters in filenames (`#`, `?`, `&`) are automatically encoded\n","skills/remotion-best-practices/rules/audio.md":"---\nname: audio\ndescription: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch\nmetadata:\n  tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx\n---\n\n# Using audio in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/media package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/media\n```\n\n## Importing Audio\n\nUse `<Audio>` from `@remotion/media` to add audio to your composition.\n\n```tsx\nimport { Audio } from \"@remotion/media\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Audio src={staticFile(\"audio.mp3\")} />;\n};\n```\n\nRemote URLs are also supported:\n\n```tsx\n<Audio src=\"https://remotion.media/audio.mp3\" />\n```\n\nBy default, audio plays from the start, at full volume and full length.\nMultiple audio tracks can be layered by adding multiple `<Audio>` components.\n\n## Trimming\n\nUse `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.\n\n```tsx\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    trimBefore={2 * fps} // Skip the first 2 seconds\n    trimAfter={10 * fps} // End at the 10 second mark\n  />\n);\n```\n\nThe audio still starts playing at the beginning of the composition - only the specified portion is played.\n\n## Delaying\n\nWrap the audio in a `<Sequence>` to delay when it starts:\n\n```tsx\nimport { Sequence, staticFile } from \"remotion\";\nimport { Audio } from \"@remotion/media\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Sequence from={1 * fps}>\n    <Audio src={staticFile(\"audio.mp3\")} />\n  </Sequence>\n);\n```\n\nThe audio will start playing after 1 second.\n\n## Volume\n\nSet a static volume (0 to 1):\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} volume={0.5} />\n```\n\nOr use a callback for dynamic volume based on the current frame:\n\n```tsx\nimport { interpolate } from \"remotion\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    volume={(f) =>\n      interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: \"clamp\" })\n    }\n  />\n);\n```\n\nThe value of `f` starts at 0 when the audio begins to play, not the composition frame.\n\n## Muting\n\nUse `muted` to silence the audio. It can be set dynamically:\n\n```tsx\nconst frame = useCurrentFrame();\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s\n  />\n);\n```\n\n## Speed\n\nUse `playbackRate` to change the playback speed:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={2} /> {/* 2x speed */}\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={0.5} /> {/* Half speed */}\n```\n\nReverse playback is not supported.\n\n## Looping\n\nUse `loop` to loop the audio indefinitely:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} loop />\n```\n\nUse `loopVolumeCurveBehavior` to control how the frame count behaves when looping:\n\n- `\"repeat\"`: Frame count resets to 0 each loop (default)\n- `\"extend\"`: Frame count continues incrementing\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  loop\n  loopVolumeCurveBehavior=\"extend\"\n  volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops\n/>\n```\n\n## Pitch\n\nUse `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={1.5} // Higher pitch\n/>\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={0.8} // Lower pitch\n/>\n```\n\nPitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.\n","skills/remotion-best-practices/rules/calculate-metadata.md":"---\nname: calculate-metadata\ndescription: Dynamically set composition duration, dimensions, and props\nmetadata:\n  tags: calculateMetadata, duration, dimensions, props, dynamic\n---\n\n# Using calculateMetadata\n\nUse `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.\n\n```tsx\n<Composition id=\"MyComp\" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />\n```\n\n## Setting duration based on a video\n\nUse the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:\n\n```tsx\nimport {CalculateMetadataFunction} from 'remotion';\nimport {getMediaMetadata} from '../get-media-metadata';\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n  };\n};\n```\n\n## Matching dimensions of a video\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n    width: dimensions?.width ?? 1920,\n    height: dimensions?.height ?? 1080,\n  };\n};\n```\n\n## Setting duration based on multiple videos\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));\n  const allMetadata = await Promise.all(metadataPromises);\n\n  const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);\n\n  return {\n    durationInFrames: Math.ceil(totalDuration * 30),\n  };\n};\n```\n\n## Setting a default outName\n\nSet the default output filename based on props:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  return {\n    defaultOutName: `video-${props.id}.mp4`,\n  };\n};\n```\n\n## Transforming props\n\nFetch data or transform props before rendering:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {\n  const response = await fetch(props.dataUrl, {signal: abortSignal});\n  const data = await response.json();\n\n  return {\n    props: {\n      ...props,\n      fetchedData: data,\n    },\n  };\n};\n```\n\nThe `abortSignal` cancels stale requests when props change in the Studio.\n\n## Return value\n\nAll fields are optional. Returned values override the `<Composition>` props:\n\n- `durationInFrames`: Number of frames\n- `width`: Composition width in pixels\n- `height`: Composition height in pixels\n- `fps`: Frames per second\n- `props`: Transformed props passed to the component\n- `defaultOutName`: Default output filename\n- `defaultCodec`: Default codec for rendering\n","skills/remotion-best-practices/rules/can-decode.md":"---\nname: can-decode\ndescription: Check if a video can be decoded by the browser using Mediabunny\nmetadata:\n  tags: decode, validation, video, audio, compatibility, browser\n---\n\n# Checking if a video can be decoded\n\nUse Mediabunny to check if a video can be decoded by the browser before attempting to play it.\n\n## The `canDecode()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const canDecode = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  try {\n    await input.getFormat();\n  } catch {\n    return false;\n  }\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (videoTrack && !(await videoTrack.canDecode())) {\n    return false;\n  }\n\n  const audioTrack = await input.getPrimaryAudioTrack();\n  if (audioTrack && !(await audioTrack.canDecode())) {\n    return false;\n  }\n\n  return true;\n};\n```\n\n## Usage\n\n```tsx\nconst src = \"https://remotion.media/video.mp4\";\nconst isDecodable = await canDecode(src);\n\nif (isDecodable) {\n  console.log(\"Video can be decoded\");\n} else {\n  console.log(\"Video cannot be decoded by this browser\");\n}\n```\n\n## Using with Blob\n\nFor file uploads or drag-and-drop, use `BlobSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, BlobSource } from \"mediabunny\";\n\nexport const canDecodeBlob = async (blob: Blob) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new BlobSource(blob),\n  });\n\n  // Same validation logic as above\n};\n```\n","skills/remotion-best-practices/rules/charts.md":"---\nname: charts\ndescription: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.\nmetadata:\n  tags: charts, data, visualization, bar-chart, pie-chart, graphs\n---\n\n# Charts in Remotion\n\nYou can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.\n\n## No animations not powered by `useCurrentFrame()`\n\nDisable all animations by third party libraries.  \nThey will cause flickering during rendering.  \nInstead, drive all animations from `useCurrentFrame()`.\n\n## Bar Chart Animations\n\nSee [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.\n\n### Staggered Bars\n\nYou can animate the height of the bars and stagger them like this:\n\n```tsx\nconst STAGGER_DELAY = 5;\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst bars = data.map((item, i) => {\n  const delay = i * STAGGER_DELAY;\n  const height = spring({\n    frame,\n    fps,\n    delay,\n    config: {damping: 200},\n  });\n  return <div style={{height: height * item.value}} />;\n});\n```\n\n## Pie Chart Animation\n\nAnimate segments using stroke-dashoffset, starting from 12 o'clock.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst progress = interpolate(frame, [0, 100], [0, 1]);\n\nconst circumference = 2 * Math.PI * radius;\nconst segmentLength = (value / total) * circumference;\nconst offset = interpolate(progress, [0, 1], [segmentLength, 0]);\n\n<circle r={radius} cx={center} cy={center} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;\n```\n","skills/remotion-best-practices/rules/compositions.md":"---\nname: compositions\ndescription: Defining compositions, stills, folders, default props and dynamic metadata\nmetadata:\n  tags: composition, still, folder, props, metadata\n---\n\nA `<Composition>` defines the component, width, height, fps and duration of a renderable video.\n\nIt normally is placed in the `src/Root.tsx` file.\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;\n};\n```\n\n## Default Props\n\nPass `defaultProps` to provide initial values for your component.  \nValues must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100}\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={\n        {\n          title: 'Hello World',\n          color: '#ff0000',\n        } satisfies MyCompositionProps\n      }\n    />\n  );\n};\n```\n\nUse `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.\n\n## Folders\n\nUse `<Folder>` to organize compositions in the sidebar.  \nFolder names can only contain letters, numbers, and hyphens.\n\n```tsx\nimport {Composition, Folder} from 'remotion';\n\nexport const RemotionRoot = () => {\n  return (\n    <>\n      <Folder name=\"Marketing\">\n        <Composition id=\"Promo\" /* ... */ />\n        <Composition id=\"Ad\" /* ... */ />\n      </Folder>\n      <Folder name=\"Social\">\n        <Folder name=\"Instagram\">\n          <Composition id=\"Story\" /* ... */ />\n          <Composition id=\"Reel\" /* ... */ />\n        </Folder>\n      </Folder>\n    </>\n  );\n};\n```\n\n## Stills\n\nUse `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.\n\n```tsx\nimport {Still} from 'remotion';\nimport {Thumbnail} from './Thumbnail';\n\nexport const RemotionRoot = () => {\n  return <Still id=\"Thumbnail\" component={Thumbnail} width={1280} height={720} />;\n};\n```\n\n## Calculate Metadata\n\nUse `calculateMetadata` to make dimensions, duration, or props dynamic based on data.\n\n```tsx\nimport {Composition, CalculateMetadataFunction} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nconst calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {\n  const data = await fetch(`https://api.example.com/video/${props.videoId}`, {\n    signal: abortSignal,\n  }).then((res) => res.json());\n\n  return {\n    durationInFrames: Math.ceil(data.duration * 30),\n    props: {\n      ...props,\n      videoUrl: data.url,\n    },\n  };\n};\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100} // Placeholder, will be overridden\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={{videoId: 'abc123'}}\n      calculateMetadata={calculateMetadata}\n    />\n  );\n};\n```\n\nThe function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/display-captions.md":"---\nname: display-captions\ndescription: Displaying captions in Remotion with TikTok-style pages and word highlighting\nmetadata:\n  tags: captions, subtitles, display, tiktok, highlight\n---\n\n# Displaying captions in Remotion\n\nThis guide explains how to display captions in Remotion, assuming you already have captions in the [`Caption`](https://www.remotion.dev/docs/captions/caption) format.\n\n## Prerequisites\n\nRead [Transcribing audio](transcribe-captions.md) for how to generate captions.\n\nFirst, the [`@remotion/captions`](https://www.remotion.dev/docs/captions) package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions\n```\n\n## Fetching captions\n\nFirst, fetch your captions JSON file. Use [`useDelayRender()`](https://www.remotion.dev/docs/use-delay-render) to hold the render until the captions are loaded:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      // Assuming captions.json is in the public/ folder.\n      const response = await fetch(staticFile(\"captions123.json\"));\n      const data = await response.json();\n      setCaptions(data);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;\n};\n```\n\n## Creating pages\n\nUse `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:\n\n```tsx\nimport { useMemo } from \"react\";\nimport { createTikTokStyleCaptions } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\n// How often captions should switch (in milliseconds)\n// Higher values = more words per page\n// Lower values = fewer words (more word-by-word)\nconst SWITCH_CAPTIONS_EVERY_MS = 1200;\n\nconst { pages } = useMemo(() => {\n  return createTikTokStyleCaptions({\n    captions,\n    combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,\n  });\n}, [captions]);\n```\n\n## Rendering with Sequences\n\nMap over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:\n\n```tsx\nimport { Sequence, useVideoConfig, AbsoluteFill } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst CaptionedContent: React.FC = () => {\n  const { fps } = useVideoConfig();\n\n  return (\n    <AbsoluteFill>\n      {pages.map((page, index) => {\n        const nextPage = pages[index + 1] ?? null;\n        const startFrame = (page.startMs / 1000) * fps;\n        const endFrame = Math.min(\n          nextPage ? (nextPage.startMs / 1000) * fps : Infinity,\n          startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,\n        );\n        const durationInFrames = endFrame - startFrame;\n\n        if (durationInFrames <= 0) {\n          return null;\n        }\n\n        return (\n          <Sequence\n            key={index}\n            from={startFrame}\n            durationInFrames={durationInFrames}\n          >\n            <CaptionPage page={page} />\n          </Sequence>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n```\n\n## White-space preservation\n\nThe captions are whitespace sensitive. You should include spaces in the `text` field before each word. Use `whiteSpace: \"pre\"` to preserve the whitespace in the captions.\n\n## Separate component for captions\n\nPut captioning logic in a separate component.  \nMake a new file for it.\n\n## Word highlighting\n\nA caption page contains `tokens` which you can use to highlight the currently spoken word:\n\n```tsx\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst HIGHLIGHT_COLOR = \"#39E508\";\n\nconst CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Current time relative to the start of the sequence\n  const currentTimeMs = (frame / fps) * 1000;\n  // Convert to absolute time by adding the page start\n  const absoluteTimeMs = page.startMs + currentTimeMs;\n\n  return (\n    <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n      <div style={{ fontSize: 80, fontWeight: \"bold\", whiteSpace: \"pre\" }}>\n        {page.tokens.map((token) => {\n          const isActive =\n            token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;\n\n          return (\n            <span\n              key={token.fromMs}\n              style={{ color: isActive ? HIGHLIGHT_COLOR : \"white\" }}\n            >\n              {token.text}\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n```\n\n## Display captions alongside video content\n\nBy default, put the captions alongside the video content, so the captions are in sync.  \nFor each video, make a new captions JSON file.\n\n```tsx\n<AbsoluteFill>\n  <Video src={staticFile(\"video.mp4\")} />\n  <CaptionPage page={page} />\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/extract-frames.md":"---\nname: extract-frames\ndescription: Extract frames from videos at specific timestamps using Mediabunny\nmetadata:\n  tags: frames, extract, video, thumbnail, filmstrip, canvas\n---\n\n# Extracting frames from videos\n\nUse Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.\n\n## The `extractFrames()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport {\n  ALL_FORMATS,\n  Input,\n  UrlSource,\n  VideoSample,\n  VideoSampleSink,\n} from \"mediabunny\";\n\ntype Options = {\n  track: { width: number; height: number };\n  container: string;\n  durationInSeconds: number | null;\n};\n\nexport type ExtractFramesTimestampsInSecondsFn = (\n  options: Options\n) => Promise<number[]> | number[];\n\nexport type ExtractFramesProps = {\n  src: string;\n  timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;\n  onVideoSample: (sample: VideoSample) => void;\n  signal?: AbortSignal;\n};\n\nexport async function extractFrames({\n  src,\n  timestampsInSeconds,\n  onVideoSample,\n  signal,\n}: ExtractFramesProps): Promise<void> {\n  using input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src),\n  });\n\n  const [durationInSeconds, format, videoTrack] = await Promise.all([\n    input.computeDuration(),\n    input.getFormat(),\n    input.getPrimaryVideoTrack(),\n  ]);\n\n  if (!videoTrack) {\n    throw new Error(\"No video track found in the input\");\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const timestamps =\n    typeof timestampsInSeconds === \"function\"\n      ? await timestampsInSeconds({\n          track: {\n            width: videoTrack.displayWidth,\n            height: videoTrack.displayHeight,\n          },\n          container: format.name,\n          durationInSeconds,\n        })\n      : timestampsInSeconds;\n\n  if (timestamps.length === 0) {\n    return;\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const sink = new VideoSampleSink(videoTrack);\n\n  for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {\n    if (signal?.aborted) {\n      break;\n    }\n\n    if (!videoSample) {\n      continue;\n    }\n\n    onVideoSample(videoSample);\n  }\n}\n```\n\n## Basic usage\n\nExtract frames at specific timestamps:\n\n```tsx\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: [0, 1, 2, 3, 4],\n  onVideoSample: (sample) => {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Creating a filmstrip\n\nUse a callback function to dynamically calculate timestamps based on video metadata:\n\n```tsx\nconst canvasWidth = 500;\nconst canvasHeight = 80;\nconst fromSeconds = 0;\nconst toSeconds = 10;\n\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: async ({ track, durationInSeconds }) => {\n    const aspectRatio = track.width / track.height;\n    const amountOfFramesFit = Math.ceil(\n      canvasWidth / (canvasHeight * aspectRatio)\n    );\n    const segmentDuration = toSeconds - fromSeconds;\n    const timestamps: number[] = [];\n\n    for (let i = 0; i < amountOfFramesFit; i++) {\n      timestamps.push(\n        fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)\n      );\n    }\n\n    return timestamps;\n  },\n  onVideoSample: (sample) => {\n    console.log(`Frame at ${sample.timestamp}s`);\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Cancellation with AbortSignal\n\nCancel frame extraction after a timeout:\n\n```tsx\nconst controller = new AbortController();\n\nsetTimeout(() => controller.abort(), 5000);\n\ntry {\n  await extractFrames({\n    src: \"https://remotion.media/video.mp4\",\n    timestampsInSeconds: [0, 1, 2, 3, 4],\n    onVideoSample: (sample) => {\n      using frame = sample;\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = frame.displayWidth;\n      canvas.height = frame.displayHeight;\n      const ctx = canvas.getContext(\"2d\");\n      frame.draw(ctx!, 0, 0);\n    },\n    signal: controller.signal,\n  });\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n\n## Timeout with Promise.race\n\n```tsx\nconst controller = new AbortController();\n\nconst timeoutPromise = new Promise<never>((_, reject) => {\n  const timeoutId = setTimeout(() => {\n    controller.abort();\n    reject(new Error(\"Frame extraction timed out after 10 seconds\"));\n  }, 10000);\n\n  controller.signal.addEventListener(\"abort\", () => clearTimeout(timeoutId), {\n    once: true,\n  });\n});\n\ntry {\n  await Promise.race([\n    extractFrames({\n      src: \"https://remotion.media/video.mp4\",\n      timestampsInSeconds: [0, 1, 2, 3, 4],\n      onVideoSample: (sample) => {\n        using frame = sample;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = frame.displayWidth;\n        canvas.height = frame.displayHeight;\n        const ctx = canvas.getContext(\"2d\");\n        frame.draw(ctx!, 0, 0);\n      },\n      signal: controller.signal,\n    }),\n    timeoutPromise,\n  ]);\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n","skills/remotion-best-practices/rules/fonts.md":"---\nname: fonts\ndescription: Loading Google Fonts and local fonts in Remotion\nmetadata:\n  tags: fonts, google-fonts, typography, text\n---\n\n# Using fonts in Remotion\n\n## Google Fonts with @remotion/google-fonts\n\nThe recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.\n\n### Prerequisites\n\nFirst, the @remotion/google-fonts package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/google-fonts # If project uses npm\nbunx remotion add @remotion/google-fonts # If project uses bun\nyarn remotion add @remotion/google-fonts # If project uses yarn\npnpm exec remotion add @remotion/google-fonts # If project uses pnpm\n```\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily } = loadFont();\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily }}>Hello World</div>;\n};\n```\n\nPreferrably, specify only needed weights and subsets to reduce file size:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Roboto\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n```\n\n### Waiting for font to load\n\nUse `waitUntilDone()` if you need to know when the font is ready:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily, waitUntilDone } = loadFont();\n\nawait waitUntilDone();\n```\n\n## Local fonts with @remotion/fonts\n\nFor local font files, use the `@remotion/fonts` package.\n\n### Prerequisites\n\nFirst, install @remotion/fonts:\n\n```bash\nnpx remotion add @remotion/fonts # If project uses npm\nbunx remotion add @remotion/fonts # If project uses bun\nyarn remotion add @remotion/fonts # If project uses yarn\npnpm exec remotion add @remotion/fonts # If project uses pnpm\n```\n\n### Loading a local font\n\nPlace your font file in the `public/` folder and use `loadFont()`:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait loadFont({\n  family: \"MyFont\",\n  url: staticFile(\"MyFont-Regular.woff2\"),\n});\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily: \"MyFont\" }}>Hello World</div>;\n};\n```\n\n### Loading multiple weights\n\nLoad each weight separately with the same family name:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait Promise.all([\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Regular.woff2\"),\n    weight: \"400\",\n  }),\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Bold.woff2\"),\n    weight: \"700\",\n  }),\n]);\n```\n\n### Available options\n\n```tsx\nloadFont({\n  family: \"MyFont\", // Required: name to use in CSS\n  url: staticFile(\"font.woff2\"), // Required: font file URL\n  format: \"woff2\", // Optional: auto-detected from extension\n  weight: \"400\", // Optional: font weight\n  style: \"normal\", // Optional: normal or italic\n  display: \"block\", // Optional: font-display behavior\n});\n```\n\n## Using in components\n\nCall `loadFont()` at the top level of your component or in a separate file that's imported early:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Montserrat\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport const Title: React.FC<{ text: string }> = ({ text }) => {\n  return (\n    <h1\n      style={{\n        fontFamily,\n        fontSize: 80,\n        fontWeight: \"bold\",\n      }}\n    >\n      {text}\n    </h1>\n  );\n};\n```\n","skills/remotion-best-practices/rules/get-audio-duration.md":"---\nname: get-audio-duration\ndescription: Getting the duration of an audio file in seconds with Mediabunny\nmetadata:\n  tags: duration, audio, length, time, seconds, mp3, wav\n---\n\n# Getting audio duration with Mediabunny\n\nMediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.\n\n## Getting audio duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getAudioDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getAudioDuration(\"https://remotion.media/audio.mp3\");\nconsole.log(duration); // e.g. 180.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getAudioDuration(staticFile(\"audio.mp3\"));\n```\n","skills/remotion-best-practices/rules/get-video-dimensions.md":"---\nname: get-video-dimensions\ndescription: Getting the width and height of a video file with Mediabunny\nmetadata:\n  tags: dimensions, width, height, resolution, size, video\n---\n\n# Getting video dimensions with Mediabunny\n\nMediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video dimensions\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDimensions = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (!videoTrack) {\n    throw new Error(\"No video track found\");\n  }\n\n  return {\n    width: videoTrack.displayWidth,\n    height: videoTrack.displayHeight,\n  };\n};\n```\n\n## Usage\n\n```tsx\nconst dimensions = await getVideoDimensions(\"https://remotion.media/video.mp4\");\nconsole.log(dimensions.width);  // e.g. 1920\nconsole.log(dimensions.height); // e.g. 1080\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst videoTrack = await input.getPrimaryVideoTrack();\nconst width = videoTrack.displayWidth;\nconst height = videoTrack.displayHeight;\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst dimensions = await getVideoDimensions(staticFile(\"video.mp4\"));\n```\n","skills/remotion-best-practices/rules/get-video-duration.md":"---\nname: get-video-duration\ndescription: Getting the duration of a video file in seconds with Mediabunny\nmetadata:\n  tags: duration, video, length, time, seconds\n---\n\n# Getting video duration with Mediabunny\n\nMediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getVideoDuration(\"https://remotion.media/video.mp4\");\nconsole.log(duration); // e.g. 10.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getVideoDuration(staticFile(\"video.mp4\"));\n```\n","skills/remotion-best-practices/rules/gifs.md":"---\nname: gif\ndescription: Displaying GIFs, APNG, AVIF and WebP in Remotion\nmetadata:\n  tags: gif, animation, images, animated, apng, avif, webp\n---\n\n# Using Animated images in Remotion\n\n## Basic usage\n\nUse `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:\n\n```tsx\nimport { AnimatedImage, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return (\n    <AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} />\n  );\n};\n```\n\nRemote URLs are also supported (must have CORS enabled):\n\n```tsx\n<AnimatedImage\n  src=\"https://example.com/animation.gif\"\n  width={500}\n  height={500}\n/>\n```\n\n## Sizing and fit\n\nControl how the image fills its container with the `fit` prop:\n\n```tsx\n// Stretch to fill (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"fill\" />\n\n// Maintain aspect ratio, fit inside container\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"contain\" />\n\n// Fill container, crop if needed\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"cover\" />\n```\n\n## Playback speed\n\nUse `playbackRate` to control the animation speed:\n\n```tsx\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}\n```\n\n## Looping behavior\n\nControl what happens when the animation finishes:\n\n```tsx\n// Loop indefinitely (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"loop\" />\n\n// Play once, show final frame\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"pause-after-finish\" />\n\n// Play once, then clear canvas\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"clear-after-finish\" />\n```\n\n## Styling\n\nUse the `style` prop for additional CSS (use `width` and `height` props for sizing):\n\n```tsx\n<AnimatedImage\n  src={staticFile(\"animation.gif\")}\n  width={500}\n  height={500}\n  style={{\n    borderRadius: 20,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n  }}\n/>\n```\n\n## Getting GIF duration\n\nUse `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.\n\n```bash\nnpx remotion add @remotion/gif\n```\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nconst duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\nconsole.log(duration); // e.g. 2.5\n```\n\nThis is useful for setting the composition duration to match the GIF:\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\n  return {\n    durationInFrames: Math.ceil(duration * 30),\n  };\n};\n```\n\n## Alternative\n\nIf `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.\n\n```bash\nnpx remotion add @remotion/gif # If project uses npm\nbunx remotion add @remotion/gif # If project uses bun\nyarn remotion add @remotion/gif # If project uses yarn\npnpm exec remotion add @remotion/gif # If project uses pnpm\n```\n\n```tsx\nimport { Gif } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Gif src={staticFile(\"animation.gif\")} width={500} height={500} />;\n};\n```\n\nThe `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.\n","skills/remotion-best-practices/rules/images.md":"---\nname: images\ndescription: Embedding images in Remotion using the <Img> component\nmetadata:\n  tags: images, img, staticFile, png, jpg, svg, webp\n---\n\n# Using images in Remotion\n\n## The `<Img>` component\n\nAlways use the `<Img>` component from `remotion` to display images:\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Img src={staticFile(\"photo.png\")} />;\n};\n```\n\n## Important restrictions\n\n**You MUST use the `<Img>` component from `remotion`.** Do not use:\n\n- Native HTML `<img>` elements\n- Next.js `<Image>` component\n- CSS `background-image`\n\nThe `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.\n\n## Local images with staticFile()\n\nPlace images in the `public/` folder and use `staticFile()` to reference them:\n\n```\nmy-video/\n├─ public/\n│  ├─ logo.png\n│  ├─ avatar.jpg\n│  └─ icon.svg\n├─ src/\n├─ package.json\n```\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\n<Img src={staticFile(\"logo.png\")} />\n```\n\n## Remote images\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n```\n\nEnsure remote images have CORS enabled.\n\nFor animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.\n\n## Sizing and positioning\n\nUse the `style` prop to control size and position:\n\n```tsx\n<Img\n  src={staticFile(\"photo.png\")}\n  style={{\n    width: 500,\n    height: 300,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n    objectFit: \"cover\",\n  }}\n/>\n```\n\n## Dynamic image paths\n\nUse template literals for dynamic file references:\n\n```tsx\nimport { Img, staticFile, useCurrentFrame } from \"remotion\";\n\nconst frame = useCurrentFrame();\n\n// Image sequence\n<Img src={staticFile(`frames/frame${frame}.png`)} />\n\n// Selecting based on props\n<Img src={staticFile(`avatars/${props.userId}.png`)} />\n\n// Conditional images\n<Img src={staticFile(`icons/${isActive ? \"active\" : \"inactive\"}.svg`)} />\n```\n\nThis pattern is useful for:\n\n- Image sequences (frame-by-frame animations)\n- User-specific avatars or profile images\n- Theme-based icons\n- State-dependent graphics\n\n## Getting image dimensions\n\nUse `getImageDimensions()` to get the dimensions of an image:\n\n```tsx\nimport { getImageDimensions, staticFile } from \"remotion\";\n\nconst { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n```\n\nThis is useful for calculating aspect ratios or sizing compositions:\n\n```tsx\nimport { getImageDimensions, staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n  return {\n    width,\n    height,\n  };\n};\n```\n","skills/remotion-best-practices/rules/import-srt-captions.md":"---\nname: import-srt-captions\ndescription: Importing .srt subtitle files into Remotion using @remotion/captions\nmetadata:\n  tags: captions, subtitles, srt, import, parse\n---\n\n# Importing .srt subtitles into Remotion\n\nIf you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.\n\nIf you don't have a .srt file, read [Transcribing audio](transcribe-captions.md) for how to generate captions instead.\n\n## Prerequisites\n\nFirst, the @remotion/captions package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions # If project uses npm\nbunx remotion add @remotion/captions # If project uses bun\nyarn remotion add @remotion/captions # If project uses yarn\npnpm exec remotion add @remotion/captions # If project uses pnpm\n```\n\n## Reading an .srt file\n\nUse `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport { parseSrt } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      const response = await fetch(staticFile(\"subtitles.srt\"));\n      const text = await response.text();\n      const { captions: parsed } = parseSrt({ input: text });\n      setCaptions(parsed);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;\n};\n```\n\nRemote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.\n\n## Using imported captions\n\nOnce parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.\n","skills/remotion-best-practices/rules/light-leaks.md":"---\nname: light-leaks\ndescription: Light leak overlay effects for Remotion using @remotion/light-leaks.\nmetadata:\n  tags: light-leaks, overlays, effects, transitions\n---\n\n## Light Leaks\n\nThis only works from Remotion 4.0.415 and up. Use `npx remotion versions` to check your Remotion version and `npx remotion upgrade` to upgrade your Remotion version.\n\n`<LightLeak>` from `@remotion/light-leaks` renders a WebGL-based light leak effect. It reveals during the first half of its duration and retracts during the second half.\n\nTypically used inside a `<TransitionSeries.Overlay>` to play over the cut point between two scenes. See the **transitions** rule for `<TransitionSeries>` and overlay usage.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/light-leaks\n```\n\n## Basic usage with TransitionSeries\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Props\n\n- `durationInFrames?` — defaults to the parent sequence/composition duration. The effect reveals during the first half and retracts during the second half.\n- `seed?` — determines the shape of the light leak pattern. Different seeds produce different patterns. Default: `0`.\n- `hueShift?` — rotates the hue in degrees (`0`–`360`). Default: `0` (yellow-to-orange). `120` = green, `240` = blue.\n\n## Customizing the look\n\n```tsx\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n// Blue-tinted light leak with a different pattern\n<LightLeak seed={5} hueShift={240} />;\n\n// Green-tinted light leak\n<LightLeak seed={2} hueShift={120} />;\n```\n\n## Standalone usage\n\n`<LightLeak>` can also be used outside of `<TransitionSeries>`, for example as a decorative overlay in any composition:\n\n```tsx\nimport { AbsoluteFill } from \"remotion\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\nconst MyComp: React.FC = () => (\n  <AbsoluteFill>\n    <MyContent />\n    <LightLeak durationInFrames={60} seed={3} />\n  </AbsoluteFill>\n);\n```\n","skills/remotion-best-practices/rules/lottie.md":"---\nname: lottie\ndescription: Embedding Lottie animations in Remotion.\nmetadata:\n  category: Animation\n---\n\n# Using Lottie Animations in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/lottie package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/lottie # If project uses npm\nbunx remotion add @remotion/lottie # If project uses bun\nyarn remotion add @remotion/lottie # If project uses yarn\npnpm exec remotion add @remotion/lottie # If project uses pnpm\n```\n\n## Displaying a Lottie file\n\nTo import a Lottie animation:\n\n- Fetch the Lottie asset\n- Wrap the loading process in `delayRender()` and `continueRender()`\n- Save the animation data in a state\n- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package\n\n```tsx\nimport {Lottie, LottieAnimationData} from '@remotion/lottie';\nimport {useEffect, useState} from 'react';\nimport {cancelRender, continueRender, delayRender} from 'remotion';\n\nexport const MyAnimation = () => {\n  const [handle] = useState(() => delayRender('Loading Lottie animation'));\n\n  const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);\n\n  useEffect(() => {\n    fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')\n      .then((data) => data.json())\n      .then((json) => {\n        setAnimationData(json);\n        continueRender(handle);\n      })\n      .catch((err) => {\n        cancelRender(err);\n      });\n  }, [handle]);\n\n  if (!animationData) {\n    return null;\n  }\n\n  return <Lottie animationData={animationData} />;\n};\n```\n\n## Styling and animating\n\nLottie supports the `style` prop to allow styles and animations:\n\n```tsx\nreturn <Lottie animationData={animationData} style={{width: 400, height: 400}} />;\n```\n\n","skills/remotion-best-practices/rules/maps.md":"---\nname: maps\ndescription: Make map animations with Mapbox\nmetadata:\n  tags: map, map animation, mapbox\n---\n\nMaps can be added to a Remotion video with Mapbox.  \nThe [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.\n\n## Prerequisites\n\nMapbox and `@turf/turf` need to be installed.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `bun.lock` is found, use the following command:\n\n```bash\nbun i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nThe user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.\n\nThe mapbox token needs to be added to the `.env` file:\n\n```txt title=\".env\"\nREMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token\n```\n\n## Adding a map\n\nHere is a basic example of a map in Remotion.\n\n```tsx\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';\nimport mapboxgl, {Map} from 'mapbox-gl';\n\nexport const lineCoordinates = [\n  [6.56158447265625, 46.059891147620725],\n  [6.5691375732421875, 46.05679376154153],\n  [6.5842437744140625, 46.05059898938315],\n  [6.594886779785156, 46.04702502069337],\n  [6.601066589355469, 46.0460718554722],\n  [6.6089630126953125, 46.0365370783104],\n  [6.6185760498046875, 46.018420689207964],\n];\n\nmapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;\n\nexport const MyComposition = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const {delayRender, continueRender} = useDelayRender();\n\n  const {width, height} = useVideoConfig();\n  const [handle] = useState(() => delayRender('Loading map...'));\n  const [map, setMap] = useState<Map | null>(null);\n\n  useEffect(() => {\n    const _map = new Map({\n      container: ref.current!,\n      zoom: 11.53,\n      center: [6.5615, 46.0598],\n      pitch: 65,\n      bearing: 0,\n      style: '⁠mapbox://styles/mapbox/standard',\n      interactive: false,\n      fadeDuration: 0,\n    });\n\n    _map.on('style.load', () => {\n      // Hide all features from the Mapbox Standard style\n      const hideFeatures = [\n        'showRoadsAndTransit',\n        'showRoads',\n        'showTransit',\n        'showPedestrianRoads',\n        'showRoadLabels',\n        'showTransitLabels',\n        'showPlaceLabels',\n        'showPointOfInterestLabels',\n        'showPointsOfInterest',\n        'showAdminBoundaries',\n        'showLandmarkIcons',\n        'showLandmarkIconLabels',\n        'show3dObjects',\n        'show3dBuildings',\n        'show3dTrees',\n        'show3dLandmarks',\n        'show3dFacades',\n      ];\n      for (const feature of hideFeatures) {\n        _map.setConfigProperty('basemap', feature, false);\n      }\n\n      _map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');\n\n      _map.addSource('trace', {\n        type: 'geojson',\n        data: {\n          type: 'Feature',\n          properties: {},\n          geometry: {\n            type: 'LineString',\n            coordinates: lineCoordinates,\n          },\n        },\n      });\n      _map.addLayer({\n        type: 'line',\n        source: 'trace',\n        id: 'line',\n        paint: {\n          'line-color': 'black',\n          'line-width': 5,\n        },\n        layout: {\n          'line-cap': 'round',\n          'line-join': 'round',\n        },\n      });\n    });\n\n    _map.on('load', () => {\n      continueRender(handle);\n      setMap(_map);\n    });\n  }, [handle, lineCoordinates]);\n\n  const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);\n\n  return <AbsoluteFill ref={ref} style={style} />;\n};\n```\n\nThe following is important in Remotion:\n\n- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.\n- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.\n- The element containing the ref MUST have an explicit width and height and `position: \"absolute\"`.\n- Do not add a `_map.remove();` cleanup function.\n\n## Drawing lines\n\nUnless I request it, do not add a glow effect to the lines.\nUnless I request it, do not add additional points to the lines.\n\n## Map style\n\nBy default, use the `mapbox://styles/mapbox/standard` style.  \nHide the labels from the base map style.\n\nUnless I request otherwise, remove all features from the Mapbox Standard style.\n\n```tsx\n// Hide all features from the Mapbox Standard style\nconst hideFeatures = [\n  'showRoadsAndTransit',\n  'showRoads',\n  'showTransit',\n  'showPedestrianRoads',\n  'showRoadLabels',\n  'showTransitLabels',\n  'showPlaceLabels',\n  'showPointOfInterestLabels',\n  'showPointsOfInterest',\n  'showAdminBoundaries',\n  'showLandmarkIcons',\n  'showLandmarkIconLabels',\n  'show3dObjects',\n  'show3dBuildings',\n  'show3dTrees',\n  'show3dLandmarks',\n  'show3dFacades',\n];\nfor (const feature of hideFeatures) {\n  _map.setConfigProperty('basemap', feature, false);\n}\n\n_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');\n_map.setConfigProperty('basemap', 'colorRoads', 'transparent');\n_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');\n```\n\n## Animating the camera\n\nYou can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.\n\nUnless I ask for it, do not jump between camera angles.\n\n```tsx\nimport * as turf from '@turf/turf';\nimport {interpolate} from 'remotion';\nimport {Easing} from 'remotion';\nimport {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';\n\nconst animationDuration = 20;\nconst cameraAltitude = 4000;\n```\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\nconst {delayRender, continueRender} = useDelayRender();\n\nuseEffect(() => {\n  if (!map) {\n    return;\n  }\n  const handle = delayRender('Moving point...');\n\n  const routeDistance = turf.length(turf.lineString(lineCoordinates));\n\n  const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {\n    easing: Easing.inOut(Easing.sin),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const camera = map.getFreeCameraOptions();\n\n  const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;\n\n  camera.lookAtPoint({\n    lng: alongRoute[0],\n    lat: alongRoute[1],\n  });\n\n  map.setFreeCameraOptions(camera);\n  map.once('idle', () => continueRender(handle));\n}, [lineCoordinates, fps, frame, handle, map]);\n```\n\nNotes:\n\nIMPORTANT: Keep the camera by default so north is up.\nIMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.\n\n- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors\n- See [Timing](./timing.md) for more options for timing.\n- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.\n\n## Animating lines\n\n### Straight lines (linear interpolation)\n\nTo animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {durationInFrames} = useVideoConfig();\n\nuseEffect(() => {\n  if (!map) return;\n\n  const animationHandle = delayRender('Animating line...');\n\n  const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  // Linear interpolation for a straight line on the map\n  const start = lineCoordinates[0];\n  const end = lineCoordinates[1];\n  const currentLng = start[0] + (end[0] - start[0]) * progress;\n  const currentLat = start[1] + (end[1] - start[1]) * progress;\n\n  const lineData: GeoJSON.Feature<GeoJSON.LineString> = {\n    type: 'Feature',\n    properties: {},\n    geometry: {\n      type: 'LineString',\n      coordinates: [start, [currentLng, currentLat]],\n    },\n  };\n\n  const source = map.getSource('trace') as mapboxgl.GeoJSONSource;\n  if (source) {\n    source.setData(lineData);\n  }\n\n  map.once('idle', () => continueRender(animationHandle));\n}, [frame, map, durationInFrames]);\n```\n\n### Curved lines (geodesic/great circle)\n\nTo animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.\n\n```tsx\nimport * as turf from '@turf/turf';\n\nconst routeLine = turf.lineString(lineCoordinates);\nconst routeDistance = turf.length(routeLine);\n\nconst currentDistance = Math.max(0.001, routeDistance * progress);\nconst slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);\n\nconst source = map.getSource('route') as mapboxgl.GeoJSONSource;\nif (source) {\n  source.setData(slicedLine);\n}\n```\n\n## Markers\n\nAdd labels, and markers where appropriate.\n\n```tsx\n_map.addSource('markers', {\n  type: 'geojson',\n  data: {\n    type: 'FeatureCollection',\n    features: [\n      {\n        type: 'Feature',\n        properties: {name: 'Point 1'},\n        geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},\n      },\n    ],\n  },\n});\n\n_map.addLayer({\n  id: 'city-markers',\n  type: 'circle',\n  source: 'markers',\n  paint: {\n    'circle-radius': 40,\n    'circle-color': '#FF4444',\n    'circle-stroke-width': 4,\n    'circle-stroke-color': '#FFFFFF',\n  },\n});\n\n_map.addLayer({\n  id: 'labels',\n  type: 'symbol',\n  source: 'markers',\n  layout: {\n    'text-field': ['get', 'name'],\n    'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],\n    'text-size': 50,\n    'text-offset': [0, 0.5],\n    'text-anchor': 'top',\n  },\n  paint: {\n    'text-color': '#FFFFFF',\n    'text-halo-color': '#000000',\n    'text-halo-width': 2,\n  },\n});\n```\n\nMake sure they are big enough. Check the composition dimensions and scale the labels accordingly.\nFor a composition size of 1920x1080, the label font size should be at least 40px.\n\nIMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:\n\n```tsx\n\"text-offset\": [0, 0.5],\n```\n\n## 3D buildings\n\nTo enable 3D buildings, use the following code:\n\n```tsx\n_map.setConfigProperty('basemap', 'show3dObjects', true);\n_map.setConfigProperty('basemap', 'show3dLandmarks', true);\n_map.setConfigProperty('basemap', 'show3dBuildings', true);\n```\n\n## Rendering\n\nWhen rendering a map animation, make sure to render with the following flags:\n\n```\nnpx remotion render --gl=angle --concurrency=1\n```\n","skills/remotion-best-practices/rules/measuring-dom-nodes.md":"---\nname: measuring-dom-nodes\ndescription: Measuring DOM element dimensions in Remotion\nmetadata:\n  tags: measure, layout, dimensions, getBoundingClientRect, scale\n---\n\n# Measuring DOM nodes in Remotion\n\nRemotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.\n\n## Measuring element dimensions\n\n```tsx\nimport { useCurrentScale } from \"remotion\";\nimport { useRef, useEffect, useState } from \"react\";\n\nexport const MyComponent = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const scale = useCurrentScale();\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    if (!ref.current) return;\n    const rect = ref.current.getBoundingClientRect();\n    setDimensions({\n      width: rect.width / scale,\n      height: rect.height / scale,\n    });\n  }, [scale]);\n\n  return <div ref={ref}>Content to measure</div>;\n};\n```\n\n","skills/remotion-best-practices/rules/measuring-text.md":"---\nname: measuring-text\ndescription: Measuring text dimensions, fitting text to containers, and checking overflow\nmetadata:\n  tags: measure, text, layout, dimensions, fitText, fillTextBox\n---\n\n# Measuring text in Remotion\n\n## Prerequisites\n\nInstall @remotion/layout-utils if it is not already installed:\n\n```bash\nnpx remotion add @remotion/layout-utils # If project uses npm\nbunx remotion add @remotion/layout-utils # If project uses bun\nyarn remotion add @remotion/layout-utils # If project uses yarn\npnpm exec remotion add @remotion/layout-utils # If project uses pnpm\n```\n\n## Measuring text dimensions\n\nUse `measureText()` to calculate the width and height of text:\n\n```tsx\nimport { measureText } from \"@remotion/layout-utils\";\n\nconst { width, height } = measureText({\n  text: \"Hello World\",\n  fontFamily: \"Arial\",\n  fontSize: 32,\n  fontWeight: \"bold\",\n});\n```\n\nResults are cached - duplicate calls return the cached result.\n\n## Fitting text to a width\n\nUse `fitText()` to find the optimal font size for a container:\n\n```tsx\nimport { fitText } from \"@remotion/layout-utils\";\n\nconst { fontSize } = fitText({\n  text: \"Hello World\",\n  withinWidth: 600,\n  fontFamily: \"Inter\",\n  fontWeight: \"bold\",\n});\n\nreturn (\n  <div\n    style={{\n      fontSize: Math.min(fontSize, 80), // Cap at 80px\n      fontFamily: \"Inter\",\n      fontWeight: \"bold\",\n    }}\n  >\n    Hello World\n  </div>\n);\n```\n\n## Checking text overflow\n\nUse `fillTextBox()` to check if text exceeds a box:\n\n```tsx\nimport { fillTextBox } from \"@remotion/layout-utils\";\n\nconst box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });\n\nconst words = [\"Hello\", \"World\", \"This\", \"is\", \"a\", \"test\"];\nfor (const word of words) {\n  const { exceedsBox } = box.add({\n    text: word + \" \",\n    fontFamily: \"Arial\",\n    fontSize: 24,\n  });\n  if (exceedsBox) {\n    // Text would overflow, handle accordingly\n    break;\n  }\n}\n```\n\n## Best practices\n\n**Load fonts first:** Only call measurement functions after fonts are loaded.\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\n\nconst { fontFamily, waitUntilDone } = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nwaitUntilDone().then(() => {\n  // Now safe to measure\n  const { width } = measureText({\n    text: \"Hello\",\n    fontFamily,\n    fontSize: 32,\n  });\n})\n```\n\n**Use validateFontIsLoaded:** Catch font loading issues early:\n\n```tsx\nmeasureText({\n  text: \"Hello\",\n  fontFamily: \"MyCustomFont\",\n  fontSize: 32,\n  validateFontIsLoaded: true, // Throws if font not loaded\n});\n```\n\n**Match font properties:** Use the same properties for measurement and rendering:\n\n```tsx\nconst fontStyle = {\n  fontFamily: \"Inter\",\n  fontSize: 32,\n  fontWeight: \"bold\" as const,\n  letterSpacing: \"0.5px\",\n};\n\nconst { width } = measureText({\n  text: \"Hello\",\n  ...fontStyle,\n});\n\nreturn <div style={fontStyle}>Hello</div>;\n```\n\n**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:\n\n```tsx\n<div style={{ outline: \"2px solid red\" }}>Text</div>\n```\n","skills/remotion-best-practices/rules/parameters.md":"---\nname: parameters\ndescription: Make a video parametrizable by adding a Zod schema\nmetadata:\n  tags: parameters, zod, schema\n---\n\nTo make a video parametrizable, a Zod schema can be added to a composition.\n\nFirst, `zod` must be installed - it must be exactly version `3.22.3`.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i zod@3.22.3\n```\n\nIf `bun.lockb` is found, use the following command:\n\n```bash\nbun i zod@3.22.3\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add zod@3.22.3\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i zod@3.22.3\n```\n\nThen, a Zod schema can be defined alongside the component:\n\n```tsx title=\"src/MyComposition.tsx\"\nimport {z} from 'zod';\n\nexport const MyCompositionSchema = z.object({\n  title: z.string(),\n});\n\nconst MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => {\n  return (\n    <div>\n      <h1>{props.title}</h1>\n    </div>\n  );\n};\n```\n\nIn the root file, the schema can be passed to the composition:\n\n```tsx title=\"src/Root.tsx\"\nimport {Composition} from 'remotion';\nimport {MycComponent, MyCompositionSchema} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{title: 'Hello World'}} schema={MyCompositionSchema} />;\n};\n```\n\nNow, the user can edit the parameter visually in the sidebar.\n\nAll schemas that are supported by Zod are supported by Remotion.\n\nRemotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.\n\n## Color picker\n\nFor adding a color picker, use `zColor()` from `@remotion/zod-types`.\n\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/zod-types # If project uses npm\nbunx remotion add @remotion/zod-types # If project uses bun\nyarn remotion add @remotion/zod-types # If project uses yarn\npnpm exec remotion add @remotion/zod-types # If project uses pnpm\n```\n\nThen import `zColor` from `@remotion/zod-types`:\n\n```tsx\nimport {zColor} from '@remotion/zod-types';\n```\n\nThen use it in the schema:\n\n```tsx\nexport const MyCompositionSchema = z.object({\n  color: zColor(),\n});\n```\n","skills/remotion-best-practices/rules/sequencing.md":"---\nname: sequencing\ndescription: Sequencing patterns for Remotion - delay, trim, limit duration of items\nmetadata:\n  tags: sequence, series, timing, delay, trim\n---\n\nUse `<Sequence>` to delay when an element appears in the timeline.\n\n```tsx\nimport { Sequence } from \"remotion\";\n\nconst {fps} = useVideoConfig();\n\n<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Title />\n</Sequence>\n<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Subtitle />\n</Sequence>\n```\n\nThis will by default wrap the component in an absolute fill element.  \nIf the items should not be wrapped, use the `layout` prop:\n\n```tsx\n<Sequence layout=\"none\">\n  <Title />\n</Sequence>\n```\n\n## Premounting\n\nThis loads the component in the timeline before it is actually played.  \nAlways premount any `<Sequence>`!\n\n```tsx\n<Sequence premountFor={1 * fps}>\n  <Title />\n</Sequence>\n```\n\n## Series\n\nUse `<Series>` when elements should play one after another without overlap.\n\n```tsx\nimport {Series} from 'remotion';\n\n<Series>\n  <Series.Sequence durationInFrames={45}>\n    <Intro />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={60}>\n    <MainContent />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={30}>\n    <Outro />\n  </Series.Sequence>\n</Series>;\n```\n\nSame as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.\n\n### Series with overlaps\n\nUse negative offset for overlapping sequences:\n\n```tsx\n<Series>\n  <Series.Sequence durationInFrames={60}>\n    <SceneA />\n  </Series.Sequence>\n  <Series.Sequence offset={-15} durationInFrames={60}>\n    {/* Starts 15 frames before SceneA ends */}\n    <SceneB />\n  </Series.Sequence>\n</Series>\n```\n\n## Frame References Inside Sequences\n\nInside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):\n\n```tsx\n<Sequence from={60} durationInFrames={30}>\n  <MyComponent />\n  {/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}\n</Sequence>\n```\n\n## Nested Sequences\n\nSequences can be nested for complex timing:\n\n```tsx\n<Sequence from={0} durationInFrames={120}>\n  <Background />\n  <Sequence from={15} durationInFrames={90} layout=\"none\">\n    <Title />\n  </Sequence>\n  <Sequence from={45} durationInFrames={60} layout=\"none\">\n    <Subtitle />\n  </Sequence>\n</Sequence>\n```\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","skills/remotion-best-practices/rules/subtitles.md":"---\nname: subtitles\ndescription: subtitles and caption rules\nmetadata:\n  tags: subtitles, captions, remotion, json\n---\n\nAll captions must be processed in JSON. The captions must use the `Caption` type which is the following:\n\n```ts\nimport type { Caption } from \"@remotion/captions\";\n```\n\nThis is the definition:\n\n```ts\ntype Caption = {\n  text: string;\n  startMs: number;\n  endMs: number;\n  timestampMs: number | null;\n  confidence: number | null;\n};\n```\n\n## Generating captions\n\nTo transcribe video and audio files to generate captions, load the [./transcribe-captions.md](./transcribe-captions.md) file for more instructions.\n\n## Displaying captions\n\nTo display captions in your video, load the [./display-captions.md](./display-captions.md) file for more instructions.\n\n## Importing captions\n\nTo import captions from a .srt file, load the [./import-srt-captions.md](./import-srt-captions.md) file for more instructions.\n","skills/remotion-best-practices/rules/tailwind.md":"---\nname: tailwind\ndescription: Using TailwindCSS in Remotion.\nmetadata:\n---\n\nYou can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.\n\nDon't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.  \n\nTailwind must be installed and enabled first in a Remotion project - fetch  https://www.remotion.dev/docs/tailwind using WebFetch for instructions.","skills/remotion-best-practices/rules/text-animations.md":"---\nname: text-animations\ndescription: Typography and text animation patterns for Remotion.\nmetadata:\n  tags: typography, text, typewriter, highlighter ken\n---\n\n## Text animations\n\nBased on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.\n\n## Typewriter Effect\n\nSee [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.\n\nAlways use string slicing for typewriter effects. Never use per-character opacity.\n\n## Word Highlighting\n\nSee [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.\n","skills/remotion-best-practices/rules/timing.md":"---\nname: timing\ndescription: Interpolation curves in Remotion - linear, easing, spring animations\nmetadata:\n  tags: spring, bounce, easing, interpolation\n---\n\nA simple linear interpolation is done using the `interpolate` function.\n\n```ts title=\"Going from 0 to 1 over 100 frames\"\nimport {interpolate} from 'remotion';\n\nconst opacity = interpolate(frame, [0, 100], [0, 1]);\n```\n\nBy default, the values are not clamped, so the value can go outside the range [0, 1].  \nHere is how they can be clamped:\n\n```ts title=\"Going from 0 to 1 over 100 frames with extrapolation\"\nconst opacity = interpolate(frame, [0, 100], [0, 1], {\n  extrapolateRight: 'clamp',\n  extrapolateLeft: 'clamp',\n});\n```\n\n## Spring animations\n\nSpring animations have a more natural motion.  \nThey go from 0 to 1 over time.\n\n```ts title=\"Spring animation from 0 to 1 over 100 frames\"\nimport {spring, useCurrentFrame, useVideoConfig} from 'remotion';\n\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst scale = spring({\n  frame,\n  fps,\n});\n```\n\n### Physical properties\n\nThe default configuration is: `mass: 1, damping: 10, stiffness: 100`.  \nThis leads to the animation having a bit of bounce before it settles.\n\nThe config can be overwritten like this:\n\n```ts\nconst scale = spring({\n  frame,\n  fps,\n  config: {damping: 200},\n});\n```\n\nThe recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.\n\nHere are some common configurations:\n\n```tsx\nconst smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)\nconst snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)\nconst bouncy = {damping: 8}; // Bouncy entrance (playful animations)\nconst heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce\n```\n\n### Delay\n\nThe animation starts immediately by default.  \nUse the `delay` parameter to delay the animation by a number of frames.\n\n```tsx\nconst entrance = spring({\n  frame: frame - ENTRANCE_DELAY,\n  fps,\n  delay: 20,\n});\n```\n\n### Duration\n\nA `spring()` has a natural duration based on the physical properties.  \nTo stretch the animation to a specific duration, use the `durationInFrames` parameter.\n\n```tsx\nconst spring = spring({\n  frame,\n  fps,\n  durationInFrames: 40,\n});\n```\n\n### Combining spring() with interpolate()\n\nMap spring output (0-1) to custom ranges:\n\n```tsx\nconst springProgress = spring({\n  frame,\n  fps,\n});\n\n// Map to rotation\nconst rotation = interpolate(springProgress, [0, 1], [0, 360]);\n\n<div style={{rotate: rotation + 'deg'}} />;\n```\n\n### Adding springs\n\nSprings return just numbers, so math can be performed:\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps, durationInFrames} = useVideoConfig();\n\nconst inAnimation = spring({\n  frame,\n  fps,\n});\nconst outAnimation = spring({\n  frame,\n  fps,\n  durationInFrames: 1 * fps,\n  delay: durationInFrames - 1 * fps,\n});\n\nconst scale = inAnimation - outAnimation;\n```\n\n## Easing\n\nEasing can be added to the `interpolate` function:\n\n```ts\nimport {interpolate, Easing} from 'remotion';\n\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nThe default easing is `Easing.linear`.  \nThere are various other convexities:\n\n- `Easing.in` for starting slow and accelerating\n- `Easing.out` for starting fast and slowing down\n- `Easing.inOut`\n\nand curves (sorted from most linear to most curved):\n\n- `Easing.quad`\n- `Easing.sin`\n- `Easing.exp`\n- `Easing.circle`\n\nConvexities and curves need be combined for an easing function:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nCubic bezier curves are also supported:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n","skills/remotion-best-practices/rules/transcribe-captions.md":"---\nname: transcribe-captions\ndescription: Transcribing audio to generate captions in Remotion\nmetadata:\n  tags: captions, transcribe, whisper, audio, speech-to-text\n---\n\n# Transcribing audio\n\nTo transcribe audio to generate captions in Remotion, you can use the [`transcribe()`](https://www.remotion.dev/docs/install-whisper-cpp/transcribe) function from the [`@remotion/install-whisper-cpp`](https://www.remotion.dev/docs/install-whisper-cpp) package.\n\n## Prerequisites\n\nFirst, the @remotion/install-whisper-cpp package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/install-whisper-cpp\n```\n\n## Transcribing\n\nMake a Node.js script to download Whisper.cpp and a model, and transcribe the audio.\n\n```ts\nimport path from \"path\";\nimport {\n  downloadWhisperModel,\n  installWhisperCpp,\n  transcribe,\n  toCaptions,\n} from \"@remotion/install-whisper-cpp\";\nimport fs from \"fs\";\n\nconst to = path.join(process.cwd(), \"whisper.cpp\");\n\nawait installWhisperCpp({\n  to,\n  version: \"1.5.5\",\n});\n\nawait downloadWhisperModel({\n  model: \"medium.en\",\n  folder: to,\n});\n\n// Convert the audio to a 16KHz wav file first if needed:\n// import {execSync} from 'child_process';\n// execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y');\n\nconst whisperCppOutput = await transcribe({\n  model: \"medium.en\",\n  whisperPath: to,\n  whisperCppVersion: \"1.5.5\",\n  inputPath: \"/path/to/audio123.wav\",\n  tokenLevelTimestamps: true,\n});\n\n// Optional: Apply our recommended postprocessing\nconst { captions } = toCaptions({\n  whisperCppOutput,\n});\n\n// Write it to the public/ folder so it can be fetched from Remotion\nfs.writeFileSync(\"captions123.json\", JSON.stringify(captions, null, 2));\n```\n\nTranscribe each clip individually and create multiple JSON files.\n\nSee [Displaying captions](display-captions.md) for how to display the captions in Remotion.\n","skills/remotion-best-practices/rules/transitions.md":"---\nname: transitions\ndescription: Scene transitions and overlays for Remotion using TransitionSeries.\nmetadata:\n  tags: transitions, overlays, fade, slide, wipe, scenes\n---\n\n## TransitionSeries\n\n`<TransitionSeries>` arranges scenes and supports two ways to enhance the cut point between them:\n\n- **Transitions** (`<TransitionSeries.Transition>`) — crossfade, slide, wipe, etc. between two scenes. Shortens the timeline because both scenes play simultaneously during the transition.\n- **Overlays** (`<TransitionSeries.Overlay>`) — render an effect (e.g. a light leak) on top of the cut point without shortening the timeline.\n\nChildren are absolutely positioned.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/transitions\n```\n\n## Transition example\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Overlay example\n\nAny React component can be used as an overlay. For a ready-made effect, see the **light-leaks** rule.\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={20}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Mixing transitions and overlays\n\nTransitions and overlays can coexist in the same `<TransitionSeries>`, but an overlay cannot be adjacent to a transition or another overlay.\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneC />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Transition props\n\n`<TransitionSeries.Transition>` requires:\n\n- `presentation` — the visual effect (e.g. `fade()`, `slide()`, `wipe()`).\n- `timing` — controls speed and easing (e.g. `linearTiming()`, `springTiming()`).\n\n## Overlay props\n\n`<TransitionSeries.Overlay>` accepts:\n\n- `durationInFrames` — how long the overlay is visible (positive integer).\n- `offset?` — shifts the overlay relative to the cut point center. Positive = later, negative = earlier. Default: `0`.\n\n## Available transition types\n\nImport transitions from their respective modules:\n\n```tsx\nimport { fade } from \"@remotion/transitions/fade\";\nimport { slide } from \"@remotion/transitions/slide\";\nimport { wipe } from \"@remotion/transitions/wipe\";\nimport { flip } from \"@remotion/transitions/flip\";\nimport { clockWipe } from \"@remotion/transitions/clock-wipe\";\n```\n\n## Slide transition with direction\n\n```tsx\nimport { slide } from \"@remotion/transitions/slide\";\n\n<TransitionSeries.Transition\n  presentation={slide({ direction: \"from-left\" })}\n  timing={linearTiming({ durationInFrames: 20 })}\n/>;\n```\n\nDirections: `\"from-left\"`, `\"from-right\"`, `\"from-top\"`, `\"from-bottom\"`\n\n## Timing options\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\n// Linear timing - constant speed\nlinearTiming({ durationInFrames: 20 });\n\n// Spring timing - organic motion\nspringTiming({ config: { damping: 200 }, durationInFrames: 25 });\n```\n\n## Duration calculation\n\nTransitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations. Overlays do **not** affect the total duration.\n\nFor example, with two 60-frame sequences and a 15-frame transition:\n\n- Without transitions: `60 + 60 = 120` frames\n- With transition: `60 + 60 - 15 = 105` frames\n\nAdding an overlay between two other sequences does not change the total.\n\n### Getting the duration of a transition\n\nUse the `getDurationInFrames()` method on the timing object:\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\nconst linearDuration = linearTiming({\n  durationInFrames: 20,\n}).getDurationInFrames({ fps: 30 });\n// Returns 20\n\nconst springDuration = springTiming({\n  config: { damping: 200 },\n}).getDurationInFrames({ fps: 30 });\n// Returns calculated duration based on spring physics\n```\n\nFor `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.\n\n### Calculating total composition duration\n\n```tsx\nimport { linearTiming } from \"@remotion/transitions\";\n\nconst scene1Duration = 60;\nconst scene2Duration = 60;\nconst scene3Duration = 60;\n\nconst timing1 = linearTiming({ durationInFrames: 15 });\nconst timing2 = linearTiming({ durationInFrames: 20 });\n\nconst transition1Duration = timing1.getDurationInFrames({ fps: 30 });\nconst transition2Duration = timing2.getDurationInFrames({ fps: 30 });\n\nconst totalDuration =\n  scene1Duration +\n  scene2Duration +\n  scene3Duration -\n  transition1Duration -\n  transition2Duration;\n// 60 + 60 + 60 - 15 - 20 = 145 frames\n```\n","skills/remotion-best-practices/rules/transparent-videos.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftransparent-videos.md -->","skills/remotion-best-practices/rules/trimming.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftrimming.md -->","skills/remotion-best-practices/rules/videos.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Fvideos.md -->","skills/remotion/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion%2FSKILL.md -->","skills/shadcn-svelte/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn-svelte%2FSKILL.md -->","skills/shadcn/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn%2FSKILL.md -->","skills/skill-creator/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2FSKILL.md -->","skills/skill-creator/references/output-patterns.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Foutput-patterns.md -->","skills/skill-creator/references/workflows.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Fworkflows.md -->","skills/solid/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsolid%2FSKILL.md -->","skills/svelte/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsvelte%2FSKILL.md -->","skills/tanstack-start/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Ftanstack-start%2FSKILL.md -->","skills/vue/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fvue%2FSKILL.md -->","skills/xstate/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fxstate%2FSKILL.md -->","skills/yaml/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fyaml%2FSKILL.md -->","skills/zustand/SKILL.md":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fzustand%2FSKILL.md -->"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nInstructions for AI coding agents working with this codebase.\n\n## Package Management\n\n**Always check the latest version before installing a package.**\n\nBefore adding or updating any dependency, verify the current latest version on npm:\n\n```bash\nnpm view <package-name> version\n```\n\nOr check multiple packages at once:\n\n```bash\nnpm view ai version\nnpm view @ai-sdk/provider-utils version\nnpm view zod version\n```\n\nThis ensures we don't install outdated versions that may have incompatible types or missing features.\n\n## Code Style\n\n- Do not use emojis in code or UI\n- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`\n- **Web app docs (`apps/web/`):** Never use Markdown table syntax (`| col | col |`). Always use HTML `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`. Markdown tables do not render correctly in the web app. Inside HTML table cells, curly braces must be escaped as JSX expressions (e.g. `<code>{'{ \"$state\": \"/path\" }'}</code>`) because MDX parses `{` as a JSX expression boundary.\n\n## AI SDK / AI Gateway\n\nWhen using the Vercel AI SDK (`ai` package) with AI Gateway, pass the model as a plain string identifier -- do not import a provider constructor:\n\n```ts\nimport { streamText } from \"ai\";\n\nconst result = streamText({\n  model: \"anthropic/claude-haiku-4.5\",\n  prompt: \"...\",\n});\n```\n\nThis requires `AI_GATEWAY_API_KEY` to be set in the environment. See `tests/e2e/` for examples.\n\n## Dev Servers\n\nAll apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.\n\nNaming convention:\n- Main web app: `json-render` → `json-render.localhost:1355`\n- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`\n\nWhen adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless my-example-demo.json-render next dev --turbopack\"\n  }\n}\n```\n\nDo **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.\n\n## Workflow\n\n- Run `pnpm type-check` after each turn to ensure type safety\n- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync.\n- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content.\n- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:\n  - Package `README.md` files in `packages/*/README.md`\n  - Root `README.md` (if packages table, install commands, or examples are affected)\n  - Web app docs in `apps/web/` (if guides, API references, or examples need updating)\n  - Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)\n  - `AGENTS.md` (if workflow or conventions change)\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nAll public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.\n\n### Preparing a release\n\nWhen asked to prepare a release (e.g. \"prepare v0.17.0\"):\n\n1. Create a branch (e.g. `prepare-v0.17.0`)\n2. Bump the version in `packages/core/package.json`\n3. Run `pnpm run version:sync` to update all other `@json-render/*` packages\n4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)\n5. **Fill documentation gaps** — every public package should have:\n   - A row in the root `README.md` packages table\n   - A renderer section in the root `README.md` (if it's a renderer)\n   - An API reference page at `apps/web/content/docs/api/<name>.mdx`\n   - An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`\n   - An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)\n   - A skill at `skills/<name>/SKILL.md`\n   - A `packages/<name>/README.md`\n6. **Run `pnpm type-check`** after all changes to verify nothing is broken\n7. Open a PR and merge to `main`\n\nCI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n### Scripts\n\n- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`\n- `pnpm run version:check` — verify all versions are in sync (runs in CI)\n- `pnpm run ci:publish` — build all packages and publish to npm (CI only)\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n","isInternal":false,"tokens":1473,"sizeBytes":5920},{"name":"SKILL.md","path":"skills/codegen/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/codegen/SKILL.md","title":"codegen","category":"anthropic-skill","format":"markdown","content":"---\nname: codegen\ndescription: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen.\n---\n\n# @json-render/codegen\n\nFramework-agnostic utilities for generating code from json-render UI trees. Use these to build custom code exporters for Next.js, Remix, or other frameworks.\n\n## Installation\n\n```bash\nnpm install @json-render/codegen\n```\n\n## Tree Traversal\n\n```typescript\nimport {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n} from \"@json-render/codegen\";\n\n// Walk the spec depth-first\ntraverseSpec(spec, (element, key, depth, parent) => {\n  console.log(`${\" \".repeat(depth * 2)}${key}: ${element.type}`);\n});\n\n// Get all component types used\nconst components = collectUsedComponents(spec);\n// Set { \"Card\", \"Metric\", \"Button\" }\n\n// Get all state paths referenced\nconst statePaths = collectStatePaths(spec);\n// Set { \"analytics/revenue\", \"user/name\" }\n\n// Get all action names\nconst actions = collectActions(spec);\n// Set { \"submit_form\", \"refresh_data\" }\n```\n\n## Serialization\n\n```typescript\nimport {\n  serializePropValue,\n  serializeProps,\n  escapeString,\n  type SerializeOptions,\n} from \"@json-render/codegen\";\n\n// Serialize a single value\nserializePropValue(\"hello\");\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue({ $state: \"/user/name\" });\n// { value: '{ $state: \"/user/name\" }', needsBraces: true }\n\n// Serialize props for JSX\nserializeProps({ title: \"Dashboard\", columns: 3, disabled: true });\n// 'title=\"Dashboard\" columns={3} disabled'\n\n// Escape strings for code\nescapeString('hello \"world\"');\n// 'hello \\\"world\\\"'\n```\n\n### SerializeOptions\n\n```typescript\ninterface SerializeOptions {\n  quotes?: \"single\" | \"double\";\n  indent?: number;\n}\n```\n\n## Types\n\n```typescript\nimport type { GeneratedFile, CodeGenerator } from \"@json-render/codegen\";\n\nconst myGenerator: CodeGenerator = {\n  generate(spec) {\n    return [\n      { path: \"package.json\", content: \"...\" },\n      { path: \"app/page.tsx\", content: \"...\" },\n    ];\n  },\n};\n```\n\n## Building a Custom Generator\n\n```typescript\nimport {\n  collectUsedComponents,\n  collectStatePaths,\n  traverseSpec,\n  serializeProps,\n  type GeneratedFile,\n} from \"@json-render/codegen\";\nimport type { Spec } from \"@json-render/core\";\n\nexport function generateNextJSProject(spec: Spec): GeneratedFile[] {\n  const files: GeneratedFile[] = [];\n  const components = collectUsedComponents(spec);\n  // Generate package.json, component files, main page...\n  return files;\n}\n```\n","frontmatter":{"name":"codegen","description":"Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen."},"isInternal":false,"tokens":615,"sizeBytes":2586},{"name":"SKILL.md","path":"skills/core/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/core/SKILL.md","title":"core","category":"anthropic-skill","format":"markdown","content":"---\nname: core\ndescription: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.\n---\n\n# @json-render/core\n\nCore package for schema definition, catalog creation, and spec streaming.\n\n## Key Concepts\n\n- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)\n- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)\n- **Spec**: JSON output from AI that conforms to the schema\n- **SpecStream**: JSONL streaming format for progressive spec building\n\n## Experimental Decision-Model Composition\n\nFor decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.\n\n- Run the Gateway evaluator server-side with `{ model: \"typesafe-ai/jev\", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.\n- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.\n- New trees default to `strategy: \"batch\"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: \"sequential\"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.\n- Batched trace steps use `choice: \"select\" | \"layout\"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.\n- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.\n- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.\n- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.\n- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.\n- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.\n- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.\n- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.\n- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.\n\nSee `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.\n\n## Defining a Schema\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nexport const schema = defineSchema((s) => ({\n  spec: s.object({\n    // Define spec structure\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate, // Optional custom AI prompt\n});\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"./schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button component\",\n    },\n  },\n});\n```\n\n## Generating AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt(); // Uses schema's promptTemplate\nconst systemPrompt = catalog.prompt({ customRules: [\"Rule 1\", \"Rule 2\"] });\n```\n\n## SpecStream Utilities\n\nFor streaming AI responses (JSONL patches):\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process streaming chunks\nconst { result, newPatches } = compiler.push(chunk);\n\n// Get final result\nconst finalSpec = compiler.getResult();\n```\n\n## Form Values in Action Handlers\n\nUse `findFormValue(\"email\", params, state)` to read a direct parameter, a dotted parameter key (such as `\"form.email\"`), a matching flat state key, or a slash-delimited path (such as `\"/form/email\"`) in nested state. Parameter values are literal, so emails and URLs containing dots are preserved. For action bindings that read nested state, use `{ $state: \"/form/email\" }`; the resolver passes that value to the handler. A bare `\"email\"` field name does not search nested `state.form.email`.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads a value from the state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - evaluates a visibility condition and picks a branch\n- **`{ \"$template\": \"Hello, ${/user/name}!\" }`** - interpolates `${/path}` references with state values\n- **`{ \"$computed\": \"fnName\", \"args\": { \"key\": <expression> } }`** - calls a registered function with resolved args\n\n`$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive).\n\nComponents do not use a `statePath` prop for two-way binding. Instead, use `{ \"$bindState\": \"/path\" }` on the natural value prop (e.g. `value`, `checked`, `pressed`).\n\n```json\n{\n  \"color\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"#007AFF\",\n    \"$else\": \"#8E8E93\"\n  },\n  \"label\": { \"$template\": \"Welcome, ${/user/name}!\" },\n  \"fullName\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\n```typescript\nimport { resolvePropValue, resolveElementProps } from \"@json-render/core\";\n\nconst resolved = resolveElementProps(element.props, { stateModel: myState });\n```\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": { \"value\": { \"$bindState\": \"/form/country\" }, \"options\": [\"US\", \"Canada\"] },\n  \"watch\": {\n    \"/form/country\": { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } }\n  },\n  \"children\": []\n}\n```\n\nWatchers only fire on value changes, not on initial render.\n\n## Validation\n\nBuilt-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`.\n\nCross-field validation uses `$state` expressions in args:\n\n```typescript\nimport { check } from \"@json-render/core\";\n\ncheck.required(\"Field is required\");\ncheck.matches(\"/form/password\", \"Passwords must match\");\ncheck.lessThan(\"/form/endDate\", \"Must be before end date\");\ncheck.greaterThan(\"/form/startDate\", \"Must be after start date\");\ncheck.requiredIf(\"/form/enableNotifications\", \"Required when enabled\");\n```\n\n## User Prompt Builder\n\nBuild structured user prompts with optional spec refinement and state context:\n\n```typescript\nimport { buildUserPrompt } from \"@json-render/core\";\n\n// Fresh generation\nbuildUserPrompt({ prompt: \"create a todo app\" });\n\n// Refinement with edit modes (default: patch-only)\nbuildUserPrompt({ prompt: \"add a toggle\", currentSpec: spec, editModes: [\"patch\", \"merge\"] });\n\n// With runtime state\nbuildUserPrompt({ prompt: \"show data\", state: { todos: [] } });\n```\n\nAvailable edit modes: `\"patch\"` (RFC 6902 JSON Patch), `\"merge\"` (RFC 7396 Merge Patch), `\"diff\"` (unified diff).\n\n## Spec Validation\n\nValidate spec structure and auto-fix common issues:\n\n```typescript\nimport { validateSpec, autoFixSpec } from \"@json-render/core\";\n\nconst { valid, issues } = validateSpec(spec);\n// issues include: missing_child, invalid_visible (malformed conditions),\n// repeat_without_children, repeat_item_outside_scope, repeat_state_mismatch\n\nconst { spec: fixed, fixDetails } = autoFixSpec(spec);\n// fixDetails entries are { message, lossy }. Lossless fixes relocate\n// misplaced fields; lossy fixes prune dangling children references.\n// In a repair loop, withhold lossy fixes until retries are exhausted:\nconst attempt = autoFixSpec(spec, { lossy: retriesExhausted });\n```\n\n## Visibility Conditions\n\nControl element visibility with state-based conditions. `VisibilityContext` is `{ stateModel: StateModel }`.\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\n// Syntax\n{ \"$state\": \"/path\" }                    // truthiness\n{ \"$state\": \"/path\", \"not\": true }      // falsy\n{ \"$state\": \"/path\", \"eq\": value }      // equality\n[ cond1, cond2 ]                         // implicit AND\n\n// Helpers\nvisibility.when(\"/path\")                 // { $state: \"/path\" }\nvisibility.unless(\"/path\")               // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)              // { $state: \"/path\", eq: val }\nvisibility.and(cond1, cond2)             // { $and: [cond1, cond2] }\nvisibility.or(cond1, cond2)              // { $or: [cond1, cond2] }\nvisibility.always                        // true\nvisibility.never                         // false\n```\n\n## Built-in Actions in Schema\n\nSchemas can declare `builtInActions` -- actions that are always available at runtime and auto-injected into prompts:\n\n```typescript\nconst schema = defineSchema(builder, {\n  builtInActions: [\n    { name: \"setState\", description: \"Update a value in the state model\" },\n  ],\n});\n```\n\nThese appear in prompts as `[built-in]` and don't require handlers in `defineRegistry`.\n\n## StateStore\n\nThe `StateStore` interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The `createStateStore` factory creates a simple in-memory implementation:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/core\";\n\nconst store = createStateStore({ count: 0 });\n\nstore.get(\"/count\");         // 0\nstore.set(\"/count\", 1);      // updates and notifies subscribers\nstore.update({ \"/a\": 1, \"/b\": 2 }); // batch update\n\nstore.subscribe(() => {\n  console.log(store.getSnapshot()); // { count: 1 }\n});\n```\n\nThe `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`, `getSnapshot()`, `subscribe(listener)`.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineSchema` | Create a new schema |\n| `defineCatalog` | Create a catalog from schema |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `resolvePropValue` | Resolve a single prop expression against data |\n| `resolveElementProps` | Resolve all prop expressions in an element |\n| `buildUserPrompt` | Build user prompts with refinement and state context |\n| `buildEditUserPrompt` | Build user prompt for editing existing specs |\n| `buildEditInstructions` | Generate prompt section for available edit modes |\n| `isNonEmptySpec` | Check if spec has root and at least one element |\n| `deepMergeSpec` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |\n| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |\n| `EditMode` | Type: `\"patch\" \\| \"merge\" \\| \"diff\"` |\n| `validateSpec` | Validate spec structure |\n| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |\n| `createSpecStreamCompiler` | Stream JSONL patches into spec |\n| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |\n| `parseSpecStreamLine` | Parse single JSONL line |\n| `applySpecStreamPatch` | Apply patch to object |\n| `StateStore` | Interface for plugging in external state management |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `check` | TypeScript helpers for creating validation checks |\n| `BuiltInAction` | Type for built-in action definitions (`name` + `description`) |\n| `ActionBinding` | Action binding type (includes `preventDefault` field) |\n","frontmatter":{"name":"core","description":"Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation."},"isInternal":false,"tokens":3434,"sizeBytes":15008},{"name":"SKILL.md","path":"skills/devtools-react/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/devtools-react/SKILL.md","title":"devtools-react","category":"anthropic-skill","format":"markdown","content":"---\nname: devtools-react\ndescription: React adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-react in a React json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-react\n\nReact adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-react\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing React json-render provider so it can observe the renderer's state, actions, and streams:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut.\n\n## Imperative controls\n\nUse `useJsonRenderDevtools` from any mounted React descendant:\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nThe hook returns `null` in production or before the component has mounted. Keep server-side stream instrumentation in `@json-render/devtools`.\n","frontmatter":{"name":"devtools-react","description":"React adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-react in a React json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking."},"isInternal":false,"tokens":380,"sizeBytes":1669},{"name":"SKILL.md","path":"skills/devtools-solid/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/devtools-solid/SKILL.md","title":"devtools-solid","category":"anthropic-skill","format":"markdown","content":"---\nname: devtools-solid\ndescription: SolidJS adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-solid in a SolidJS json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-solid\n\nSolidJS adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing SolidJS json-render provider:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","frontmatter":{"name":"devtools-solid","description":"SolidJS adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-solid in a SolidJS json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking."},"isInternal":false,"tokens":274,"sizeBytes":1214},{"name":"SKILL.md","path":"skills/devtools-svelte/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/devtools-svelte/SKILL.md","title":"devtools-svelte","category":"anthropic-skill","format":"markdown","content":"---\nname: devtools-svelte\ndescription: Svelte 5 adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-svelte in a Svelte json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-svelte\n\nSvelte 5 adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-svelte\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Svelte json-render provider:\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","frontmatter":{"name":"devtools-svelte","description":"Svelte 5 adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-svelte in a Svelte json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking."},"isInternal":false,"tokens":279,"sizeBytes":1177},{"name":"SKILL.md","path":"skills/devtools-vue/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/devtools-vue/SKILL.md","title":"devtools-vue","category":"anthropic-skill","format":"markdown","content":"---\nname: devtools-vue\ndescription: Vue adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-vue in a Vue json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-vue\n\nVue adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-vue\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Vue json-render provider:\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n","frontmatter":{"name":"devtools-vue","description":"Vue adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-vue in a Vue json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking."},"isInternal":false,"tokens":290,"sizeBytes":1229},{"name":"SKILL.md","path":"skills/devtools/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/devtools/SKILL.md","title":"devtools","category":"anthropic-skill","format":"markdown","content":"---\nname: devtools\ndescription: Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include \"add devtools\", \"debug json-render\", \"inspect the spec\", \"why is this element not rendering\", \"see the state at runtime\", or requests to tap streams / capture action logs for `@json-render/devtools`.\n---\n\n# @json-render/devtools\n\nA floating inspector panel for json-render apps. Framework-agnostic core + per-framework adapters (React, Vue, Svelte, Solid).\n\nProduction-safe: the component renders no devtools UI when `process.env.NODE_ENV === \"production\"`, including when a bundler replaces the expression in a browser without a `process` global. Unbundled environments without `process.env` retain development behavior.\n\n## Install\n\nInstall the core package plus the adapter that matches the host app's renderer.\n\n```bash\n# React\nnpm install @json-render/devtools @json-render/devtools-react\n\n# Vue\nnpm install @json-render/devtools @json-render/devtools-vue\n\n# Svelte\nnpm install @json-render/devtools @json-render/devtools-svelte\n\n# Solid\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Drop-in usage\n\nPlace `<JsonRenderDevtools />` anywhere inside the existing `<JSONUIProvider>` (or framework equivalent). No other wiring required.\n\n### React\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\n### Vue\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\n### Svelte\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\n### Solid\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\n## Controls\n\n- Floating toggle appears bottom-right.\n- Hotkey: `Ctrl`/`Cmd` + `Shift` + `J` (configurable via `hotkey` prop).\n- Drawer is resizable; height persists to localStorage.\n\n## Props\n\n- `spec` (`Spec | null`) — current spec.\n- `catalog` (`Catalog | null`) — catalog definition; required for the Catalog panel.\n- `messages` (`UIMessage[]`) — AI SDK `useChat` messages; scanned for spec data parts.\n- `initialOpen` (`boolean`) — start open.\n- `position` (`\"bottom-right\" | \"bottom-left\" | \"right\"`) — dock + toggle corner. `\"bottom-*\"` docks at the bottom; `\"right\"` docks at the right edge full-height (recommended for app-shells that already use `100vh` or fixed bottom bars).\n- `hotkey` (`string | false`) — `\"mod+shift+j\"` by default.\n- `bufferSize` (`number`) — event ring-buffer cap, default 500.\n- `reserveSpace` (`boolean`, default `true`) — when true the panel pushes the host app by applying `padding-bottom` / `padding-right` on `body`. Set to `false` to keep the panel as a pure overlay.\n- `allowDockToggle` (`boolean`, default `true`) — show a toolbar button so the user can flip the panel between bottom-dock and right-dock. User choice persists to `localStorage` and overrides `position` on subsequent mounts. Pass `false` to lock the dock to `position`.\n- `onEvent` (`(DevtoolsEvent) => void`) — optional tap.\n\n## Panels\n\n- **Spec** — element tree rooted at `spec.root`; props/visibility/events/watchers detail; integrated `validateSpec` warnings.\n- **State** — every JSON Pointer path with inline edit via `store.set`.\n- **Actions** — dispatched actions timeline (name, params, result/error, duration).\n- **Stream** — spec patches, text chunks, token usage, lifecycle markers grouped by generation.\n- **Catalog** — components + actions declared in the catalog with prop chips.\n\n## Picker (toolbar)\n\nThe element picker is a toolbar button in the panel header (Chrome-DevTools-style), not a tab. Click it to activate pick mode, then click any rendered element in the page — selection jumps to the Spec tab with that element focused. `Esc` cancels.\n\n## Reserved space & docking\n\nThe panel can dock at the bottom or the right edge, and by default the user can flip between the two with a toolbar button (the choice persists to `localStorage`). Set `allowDockToggle={false}` if the host app only works with one dock — the button is hidden and the dock is locked to `position`.\n\nPick an initial dock that fits your layout:\n\n- **Bottom dock (default)** — works best for docs / marketing / content-flow sites and for app shells built with a `height: 100%` chain (`html { height: 100% }` → `body { height: 100% }` → `.app { height: 100% }`). The panel writes its height to `--jr-devtools-offset-bottom` and applies matching `padding-bottom` to `body`, so non-fixed content naturally makes room.\n- **Right dock** (`position=\"right\"`) — recommended for app-shell layouts that use `100vh` or `position: fixed; bottom: 0`. Right docking sidesteps the bottom edge entirely and writes its width to `--jr-devtools-offset-right` instead.\n\nApps that use `100vh`, `position: fixed`, or `position: sticky` can opt specific elements in with the published CSS custom properties:\n\n```css\n.composer   { bottom: var(--jr-devtools-offset-bottom, 0); }\n.sidebar    { right:  var(--jr-devtools-offset-right,  0); }\n.app-shell  { height: calc(100vh - var(--jr-devtools-offset-bottom, 0)); }\n```\n\nIf the automatic body padding causes problems with a particular layout, pass `reserveSpace={false}` to make the panel a pure overlay — the CSS custom properties are still published so you can reserve space manually.\n\n(`--jr-devtools-offset` is kept as a back-compat alias for whichever edge is currently active.)\n\n## Multiple renderers on one page (e.g. a chat)\n\nA single `<JsonRenderDevtools />` can inspect many `<Renderer />` instances at once — a chat where each assistant message renders its own spec, a dashboard made of several independent widgets, etc. The recipe:\n\n1. **One top-level `<JSONUIProvider>`** so every renderer shares one state store and one action dispatcher. Devtools lives inside this provider and sees everything through it.\n2. **Per-renderer specs, shared state** — each assistant message renders `<Renderer spec={msgSpec} registry={registry} />` directly, not wrapped in its own `StateProvider`. State paths from different messages must not collide.\n3. **Namespace state per turn** — when the source is an AI stream, hand the agent a unique `messageId` and require every element key (`<id>-root`) and state path (`/<id>/count`) to be prefixed with it.\n4. **Pass `spec={latest}` + `messages={all}`** — `spec` drives the Spec panel (usually the newest assistant message's spec), while `messages` feeds the Stream panel with patches from every turn.\n5. **Actions and the picker are already global** — `registerActionObserver` captures dispatches from any `ActionProvider` in the tree, and `data-jr-key` is written by the renderer itself, so Pick works across every rendered element regardless of which message produced it.\n\nSee `examples/devtools` for a full AI chat wired this way.\n\n## Imperative API (React only)\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nReturns `null` in production or before the component mounts.\n\n## Server-side stream tap\n\nCapture spec patches at the API route so events persist server-side or flow into your own telemetry.\n\n```ts\nimport { tapJsonRenderStream, createEventStore } from \"@json-render/devtools\";\nimport { pipeJsonRender } from \"@json-render/core\";\n\nconst events = createEventStore({ bufferSize: 1000 });\nconst tapped = tapJsonRenderStream(result.toUIMessageStream(), events);\nwriter.merge(pipeJsonRender(tapped));\n```\n\nYAML equivalent: `tapYamlStream`.\n\n## Under the hood\n\n- **Shadow-DOM isolated panel** — the panel's styles never leak into the host app and vice versa.\n- **Ring-buffered event store** — capped log of devtools events (state changes, action dispatches, stream patches, etc.).\n- **Action observer registry** — each framework's `ActionProvider` reports via `notifyActionDispatch` / `notifyActionSettle` in `@json-render/core`; devtools subscribes via `registerActionObserver`.\n- **Picker element tagging** — while devtools is mounted, `ElementRenderer` wraps each rendered element in `<span data-jr-key=\"...\" style=\"display:contents\">` so the picker can map DOM → spec key. No layout impact.\n","frontmatter":{"name":"devtools","description":"Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include \"add devtools\", \"debug json-render\", \"inspect the spec\", \"why is this element not rendering\", \"see the state at runtime\", or requests to tap streams / capture action logs for `@json-render/devtools`."},"isInternal":false,"tokens":2194,"sizeBytes":9240},{"name":"SKILL.md","path":"skills/directives/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/directives/SKILL.md","title":"directives","category":"anthropic-skill","format":"markdown","content":"---\nname: directives\ndescription: Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs.\n---\n\n# @json-render/directives\n\nPre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.\n\n## Quick Start\n\n```typescript\nimport { standardDirectives } from '@json-render/directives';\n\n// Wire into prompt generation\nconst prompt = catalog.prompt({ directives: standardDirectives });\n\n// Wire into the renderer (React example)\nimport { JSONUIProvider, Renderer } from '@json-render/react';\n\n<JSONUIProvider registry={registry} directives={standardDirectives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\nTo add factory directives like `createI18nDirective`, spread the array:\n\n```typescript\nimport { standardDirectives, createI18nDirective } from '@json-render/directives';\n\nconst directives = [...standardDirectives, createI18nDirective(config)];\n```\n\n## Defining Custom Directives\n\nUse `defineDirective` from `@json-render/core`:\n\n```typescript\nimport { defineDirective, resolvePropValue } from '@json-render/core';\nimport { z } from 'zod';\n\nconst doubleDirective = defineDirective({\n  name: '$double',\n  description: 'Double a numeric value.',\n  schema: z.object({\n    $double: z.unknown(),\n  }),\n  resolve(value, ctx) {\n    const resolved = resolvePropValue(value.$double, ctx);\n    return (resolved as number) * 2;\n  },\n});\n```\n\nRules:\n- Name must start with `$`\n- Name must not conflict with built-in keys (`$state`, `$cond`, `$computed`, `$template`, `$item`, `$index`, `$bindState`, `$bindItem`)\n- Resolvers should call `resolvePropValue` on sub-values to support composition\n\n## Built-in Directives\n\n### `$format` — Locale-aware value formatting\n\nFormats values using `Intl` formatters. Supports `date`, `currency`, `number`, and `percent`.\n\n```json\n{ \"$format\": \"currency\", \"value\": { \"$state\": \"/cart/total\" }, \"currency\": \"USD\" }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/user/createdAt\" } }\n{ \"$format\": \"number\", \"value\": 1234567, \"notation\": \"compact\" }\n{ \"$format\": \"percent\", \"value\": 0.75 }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/post/createdAt\" }, \"style\": \"relative\" }\n```\n\nFields: `$format` (date | currency | number | percent), `value` (any expression), `locale?` (string), `currency?` (string, default \"USD\"), `notation?` (string), `style?` (\"relative\" for relative dates), `options?` (extra Intl options).\n\n### `$math` — Arithmetic operations\n\n```json\n{ \"$math\": \"add\", \"a\": { \"$state\": \"/subtotal\" }, \"b\": { \"$state\": \"/tax\" } }\n{ \"$math\": \"round\", \"a\": 3.7 }\n```\n\nOperations: `add`, `subtract`, `multiply`, `divide`, `mod`, `min`, `max`, `round`, `floor`, `ceil`, `abs`. Unary ops (`round`, `floor`, `ceil`, `abs`) only use `a`. Division by zero returns `0`.\n\nFields: `$math` (operation enum), `a?` (first operand, default 0), `b?` (second operand, default 0).\n\n### `$concat` — String concatenation\n\n```json\n{ \"$concat\": [{ \"$state\": \"/user/firstName\" }, \" \", { \"$state\": \"/user/lastName\" }] }\n```\n\nFields: `$concat` (array of values to resolve and join into a string).\n\n### `$count` — Array/string length\n\n```json\n{ \"$count\": { \"$state\": \"/cart/items\" } }\n```\n\nReturns `.length` of arrays or strings, `0` for other types.\n\nFields: `$count` (value to count).\n\n### `$truncate` — Text truncation\n\n```json\n{ \"$truncate\": { \"$state\": \"/post/body\" }, \"length\": 140, \"suffix\": \"...\" }\n```\n\nFields: `$truncate` (value to truncate), `length?` (max chars, default 100), `suffix?` (string, default \"...\").\n\n### `$pluralize` — Singular/plural forms\n\n```json\n{ \"$pluralize\": { \"$state\": \"/cart/itemCount\" }, \"one\": \"item\", \"other\": \"items\", \"zero\": \"no items\" }\n```\n\nOutput: `\"3 items\"`, `\"1 item\"`, or `\"no items\"`.\n\nFields: `$pluralize` (count value), `one` (singular label), `other` (plural label), `zero?` (zero label).\n\n### `$join` — Join array elements\n\n```json\n{ \"$join\": { \"$state\": \"/tags\" }, \"separator\": \", \" }\n```\n\nFields: `$join` (array to join), `separator?` (string, default \", \").\n\n### `createI18nDirective` — Internationalization factory\n\n```typescript\nimport { createI18nDirective } from '@json-render/directives';\n\nconst tDirective = createI18nDirective({\n  locale: 'en',\n  messages: {\n    en: { \"greeting\": \"Hello, {{name}}!\", \"checkout.submit\": \"Place Order\" },\n    es: { \"greeting\": \"Hola, {{name}}!\", \"checkout.submit\": \"Realizar Pedido\" },\n  },\n  fallbackLocale: 'en',\n});\n```\n\nUsage in specs:\n\n```json\n{ \"$t\": \"checkout.submit\" }\n{ \"$t\": \"greeting\", \"params\": { \"name\": { \"$state\": \"/user/name\" } } }\n```\n\nFields: `$t` (translation key), `params?` (interpolation parameters, values accept expressions).\n\nConfig: `locale` (current locale), `messages` (Record<locale, Record<key, string>>), `fallbackLocale?` (fallback when key missing).\n\n## Composition\n\nDirectives compose naturally — each resolver calls `resolvePropValue` on its inputs, so directives can wrap other directives or built-in expressions:\n\n```json\n{\n  \"$format\": \"currency\",\n  \"value\": { \"$math\": \"multiply\", \"a\": { \"$state\": \"/price\" }, \"b\": { \"$state\": \"/qty\" } },\n  \"currency\": \"USD\"\n}\n```\n\nResolves inside-out: `$state` reads from state, `$math` multiplies, `$format` formats as currency.\n\n## Wiring into Renderers\n\nAll four renderers (React, Vue, Svelte, Solid) accept `directives` on their provider and `createRenderer` output:\n\n```tsx\n// Provider pattern\n<JSONUIProvider registry={registry} directives={directives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n\n// createRenderer pattern\nconst MyRenderer = createRenderer(catalog, components);\n<MyRenderer spec={spec} directives={directives} />\n```\n\nFor prompt generation, pass the same array:\n\n```typescript\nconst prompt = catalog.prompt({ directives });\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `formatDirective` | `$format` directive definition |\n| `mathDirective` | `$math` directive definition |\n| `concatDirective` | `$concat` directive definition |\n| `countDirective` | `$count` directive definition |\n| `truncateDirective` | `$truncate` directive definition |\n| `pluralizeDirective` | `$pluralize` directive definition |\n| `joinDirective` | `$join` directive definition |\n| `createI18nDirective` | Factory for `$t` i18n directive |\n| `standardDirectives` | Array of all 7 non-factory directives |\n| `I18nConfig` | Type for i18n configuration |\n","frontmatter":{"name":"directives","description":"Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs."},"isInternal":false,"tokens":1761,"sizeBytes":6613},{"name":"SKILL.md","path":"skills/image/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/image/SKILL.md","title":"image","category":"anthropic-skill","format":"markdown","content":"---\nname: image\ndescription: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.\n---\n\n# @json-render/image\n\nImage renderer that converts JSON specs into SVG and PNG images using Satori.\n\n## Quick Start\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"frame\",\n  elements: {\n    frame: {\n      type: \"Frame\",\n      props: { width: 1200, height: 630, backgroundColor: \"#1a1a2e\" },\n      children: [\"heading\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Hello World\", level: \"h1\", color: \"#ffffff\" },\n      children: [],\n    },\n  },\n};\n\nconst png = await renderToPng(spec, {\n  fonts: [{ name: \"Inter\", data: fontData, weight: 400, style: \"normal\" }],\n});\n```\n\n## Using Standard Components\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, standardComponentDefinitions } from \"@json-render/image\";\n\nexport const imageCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Adding Custom Components\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n});\n```\n\n## Standard Components\n\n| Component | Category | Description |\n|-----------|----------|-------------|\n| `Frame` | Root | Root container. Defines width, height, background. Must be root. |\n| `Box` | Layout | Container with padding, margin, border, absolute positioning |\n| `Row` | Layout | Horizontal flex layout |\n| `Column` | Layout | Vertical flex layout |\n| `Heading` | Content | h1-h4 heading text |\n| `Text` | Content | Body text with full styling |\n| `Image` | Content | Image from URL |\n| `Divider` | Decorative | Horizontal line separator |\n| `Spacer` | Decorative | Empty vertical space |\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `renderToSvg` | Render spec to SVG string |\n| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) |\n| `schema` | Image element schema |\n| `standardComponents` | Pre-built component registry |\n| `standardComponentDefinitions` | Catalog definitions for AI prompts |\n\n## Sub-path Exports\n\n| Export | Description |\n|--------|-------------|\n| `@json-render/image` | Full package: schema, components, render functions |\n| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) |\n| `@json-render/image/catalog` | Standard component definitions and types |\n| `@json-render/image/render` | Render functions only |\n","frontmatter":{"name":"image","description":"Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs."},"isInternal":false,"tokens":686,"sizeBytes":2875},{"name":"SKILL.md","path":"skills/ink/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/ink/SKILL.md","title":"ink","category":"anthropic-skill","format":"markdown","content":"---\nname: ink\ndescription: Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.\n---\n\n# @json-render/ink\n\nInk terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/ink/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/ink/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/ink\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    CustomWidget: {\n      props: z.object({ title: z.string() }),\n      slots: [],\n      description: \"Custom widget\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    CustomWidget: ({ props }) => <Text>{props.title}</Text>,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <JSONUIProvider initialState={{}}>\n      <Renderer spec={spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Spec Structure (Flat Element Map)\n\nThe Ink schema uses a flat element map with a root key:\n\n```json\n{\n  \"root\": \"main\",\n  \"elements\": {\n    \"main\": {\n      \"type\": \"Box\",\n      \"props\": { \"flexDirection\": \"column\", \"padding\": 1 },\n      \"children\": [\"heading\", \"content\"]\n    },\n    \"heading\": {\n      \"type\": \"Heading\",\n      \"props\": { \"text\": \"Dashboard\", \"level\": \"h1\" },\n      \"children\": []\n    },\n    \"content\": {\n      \"type\": \"Text\",\n      \"props\": { \"text\": \"Hello from the terminal!\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Standard Components\n\n### Layout\n- `Box` - Flexbox layout container (like a terminal `<div>`). Use for grouping, spacing, borders, alignment. Default flexDirection is row.\n- `Text` - Text output with optional styling (color, bold, italic, etc.)\n- `Newline` - Inserts blank lines. Must be inside a Box with flexDirection column.\n- `Spacer` - Flexible empty space that expands along the main axis.\n\n### Content\n- `Heading` - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)\n- `Divider` - Horizontal separator with optional centered title\n- `Badge` - Colored inline label (variants: default, info, success, warning, error)\n- `Spinner` - Animated loading spinner with optional label\n- `ProgressBar` - Horizontal progress bar (0-1)\n- `Sparkline` - Inline chart using Unicode block characters\n- `BarChart` - Horizontal bar chart with labels and values\n- `Table` - Tabular data with headers and rows\n- `List` - Bulleted or numbered list\n- `ListItem` - Structured list row with title, subtitle, leading/trailing text\n- `Card` - Bordered container with optional title\n- `KeyValue` - Key-value pair display\n- `Link` - Clickable URL with optional label\n- `StatusLine` - Status message with colored icon (info, success, warning, error)\n- `Markdown` - Renders markdown text with terminal styling\n\n### Interactive\n- `TextInput` - Text input field (events: submit, change)\n- `Select` - Selection menu with arrow key navigation (events: change)\n- `MultiSelect` - Multi-selection with space to toggle (events: change, submit)\n- `ConfirmInput` - Yes/No confirmation prompt (events: confirm, deny)\n- `Tabs` - Tab bar navigation with left/right arrow keys (events: change)\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop of form components\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Event System\n\nComponents use `emit` to fire named events. The element's `on` field maps events to action bindings:\n\n```tsx\nCustomButton: ({ props, emit }) => (\n  <Box>\n    <Text>{props.label}</Text>\n    {/* emit(\"press\") triggers the action bound in the spec's on.press */}\n  </Box>\n),\n```\n\n```json\n{\n  \"type\": \"CustomButton\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\n`setState`, `pushState`, and `removeState` are built-in and handled automatically:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n```\n\n## Repeat (Dynamic Lists)\n\nUse the `repeat` field on a container element to render items from a state array:\n\n```json\n{\n  \"type\": \"Box\",\n  \"props\": { \"flexDirection\": \"column\" },\n  \"repeat\": { \"statePath\": \"/items\", \"key\": \"id\" },\n  \"children\": [\"item-row\"]\n}\n```\n\nInside repeated children, use `{ \"$item\": \"field\" }` to read from the current item and `{ \"$index\": true }` for the current index.\n\nFor nested lists, an inner repeat can use `{ \"statePath\": { \"$item\": \"children\" } }` to iterate an array on the enclosing item.\n\n## Streaming\n\nUse `useUIStream` to progressively render specs from JSONL patch streams:\n\n```tsx\nimport { useUIStream } from \"@json-render/ink\";\n\nconst { spec, send, isStreaming } = useUIStream({ api: \"/api/generate\" });\n```\n\n## Server-Side Prompt Generation\n\nUse the `./server` export to generate AI system prompts from your catalog:\n\n```typescript\nimport { catalog } from \"./catalog\";\n\nconst systemPrompt = catalog.prompt({ system: \"You are a terminal assistant.\" });\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched via the event system |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n| `FocusProvider` | Manage focus across interactive components |\n| `JSONUIProvider` | Combined provider for all contexts |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/ink\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## createRenderer (Higher-Level API)\n\n```tsx\nimport { createRenderer } from \"@json-render/ink\";\nimport { standardComponents } from \"@json-render/ink\";\nimport { catalog } from \"./catalog\";\n\nconst InkRenderer = createRenderer(catalog, {\n  ...standardComponents,\n  // custom component overrides here\n});\n\n// InkRenderer includes all providers (state, visibility, actions, focus)\nrender(\n  <InkRenderer spec={spec} state={{ activeTab: \"overview\" }} />\n);\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `createRenderer` | Higher-level: creates a component with built-in providers |\n| `JSONUIProvider` | Combined provider for all contexts |\n| `schema` | Ink flat element map schema (includes built-in state actions) |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way binding for `$bindState`/`$bindItem` expressions |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useOptionalValidation` | Non-throwing variant of useValidation |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n| `Components` | Typed component map (catalog-aware) |\n| `Actions` | Typed action map (catalog-aware) |\n| `ComponentContext` | Typed component context (catalog-aware) |\n| `flatToTree` | Convert flat element map to tree structure |\n\n## Terminal UI Design Guidelines\n\n- Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.\n- Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.\n- Use borderStyle on Box for visual grouping (single, double, round, bold).\n- Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.\n- Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.\n- Use Tabs for multi-view UIs with visible conditions on child content.\n- Use Sparkline for inline trends and BarChart for comparing values.\n","frontmatter":{"name":"ink","description":"Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal."},"isInternal":false,"tokens":2443,"sizeBytes":9934},{"name":"SKILL.md","path":"skills/jotai/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/jotai/SKILL.md","title":"jotai","category":"anthropic-skill","format":"markdown","content":"---\nname: jotai\ndescription: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.\n---\n\n# @json-render/jotai\n\nJotai adapter for json-render's `StateStore` interface. Wire a Jotai atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/jotai @json-render/core @json-render/react jotai\n```\n\n## Usage\n\n```tsx\nimport { atom } from \"jotai\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom that holds the json-render state\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = jotaiStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Jotai */}\n</StateProvider>\n```\n\n### With a Shared Jotai Store\n\nWhen your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:\n\n```tsx\nimport { atom, createStore } from \"jotai\";\nimport { Provider as JotaiProvider } from \"jotai/react\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst jStore = createStore();\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\nconst store = jotaiStateStore({ atom: uiAtom, store: jStore });\n\n<JotaiProvider store={jStore}>\n  <StateProvider store={store}>\n    {/* Both json-render and useAtom() see the same state */}\n  </StateProvider>\n</JotaiProvider>\n```\n\n## API\n\n### `jotaiStateStore(options)`\n\nCreates a `StateStore` backed by a Jotai atom.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `WritableAtom<StateModel, [StateModel], void>` | Yes | A writable atom holding the state model |\n| `store` | Jotai `Store` | No | The Jotai store instance. Defaults to a new store. Pass your own to share state with `<Provider>`. |\n","frontmatter":{"name":"jotai","description":"Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai."},"isInternal":false,"tokens":528,"sizeBytes":2023},{"name":"SKILL.md","path":"skills/mcp/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/mcp/SKILL.md","title":"mcp","category":"anthropic-skill","format":"markdown","content":"---\nname: mcp\ndescription: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.\n---\n\n# @json-render/mcp\n\nMCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Quick Start\n\n### Server (Node.js)\n\n```typescript\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...shadcnComponentDefinitions },\n  actions: {},\n});\n\nconst server = createMcpApp({\n  name: \"My App\",\n  version: \"1.0.0\",\n  catalog,\n  html: fs.readFileSync(\"dist/index.html\", \"utf-8\"),\n});\n\nawait server.connect(new StdioServerTransport());\n```\n\n### Client (React, inside iframe)\n\n```tsx\nimport { useJsonRenderApp } from \"@json-render/mcp/app\";\nimport { JSONUIProvider, Renderer } from \"@json-render/react\";\n\nfunction McpAppView({ registry }) {\n  const { spec, loading, error } = useJsonRenderApp();\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting...</div>;\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Architecture\n\n1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource\n2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs\n3. The HTML resource is a Vite-bundled single-file React app with json-render renderers\n4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs\n\n## Server API\n\n- `createMcpApp(options)` - main entry, creates a full MCP server\n- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server\n- `registerJsonRenderResource(server, options)` - register the UI resource\n\n## Client API (`@json-render/mcp/app`)\n\n- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`\n- `buildAppHtml(options)` - generate HTML from bundled JS/CSS\n\n## Building the iframe HTML\n\nBundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\n\nexport default defineConfig({\n  plugins: [react(), viteSingleFile()],\n  build: { outDir: \"dist\" },\n});\n```\n\n## Client Configuration\n\n### Cursor (`.cursor/mcp.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## Dependencies\n\n```bash\n# Server\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n\n# Client (iframe)\nnpm install @json-render/react @json-render/shadcn react react-dom\n\n# Build tools\nnpm install -D vite @vitejs/plugin-react vite-plugin-singlefile\n```\n","frontmatter":{"name":"mcp","description":"MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol."},"isInternal":false,"tokens":898,"sizeBytes":3528},{"name":"SKILL.md","path":"skills/next/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/next/SKILL.md","title":"next","category":"anthropic-skill","format":"markdown","content":"---\nname: next\ndescription: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.\n---\n\n# @json-render/next\n\nNext.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.\n\n## Quick Start\n\n```bash\nnpm install @json-render/core @json-render/react @json-render/next\n```\n\n### 1. Define Your Spec\n\n```typescript\n// lib/spec.ts\nimport type { NextAppSpec } from \"@json-render/next\";\n\nexport const spec: NextAppSpec = {\n  metadata: {\n    title: { default: \"My App\", template: \"%s | My App\" },\n    description: \"A json-render Next.js application\",\n  },\n  layouts: {\n    main: {\n      root: \"shell\",\n      elements: {\n        shell: { type: \"Container\", props: {}, children: [\"nav\", \"slot\"] },\n        nav: { type: \"NavBar\", props: { links: [\n          { href: \"/\", label: \"Home\" },\n          { href: \"/about\", label: \"About\" },\n        ]}, children: [] },\n        slot: { type: \"Slot\", props: {}, children: [] },\n      },\n    },\n  },\n  routes: {\n    \"/\": {\n      layout: \"main\",\n      metadata: { title: \"Home\" },\n      page: {\n        root: \"hero\",\n        elements: {\n          hero: { type: \"Card\", props: { title: \"Welcome\" }, children: [] },\n        },\n      },\n    },\n    \"/about\": {\n      layout: \"main\",\n      metadata: { title: \"About\" },\n      page: {\n        root: \"content\",\n        elements: {\n          content: { type: \"Card\", props: { title: \"About Us\" }, children: [] },\n        },\n      },\n    },\n  },\n};\n```\n\n### 2. Create the App\n\n```typescript\n// lib/app.ts\nimport { createNextApp } from \"@json-render/next/server\";\nimport { spec } from \"./spec\";\n\nexport const { Page, generateMetadata, generateStaticParams } = createNextApp({\n  spec,\n  loaders: {\n    // Server-side data loaders (optional)\n    loadPost: async ({ slug }) => {\n      const post = await getPost(slug as string);\n      return { post };\n    },\n  },\n});\n```\n\n### 3. Wire Up Route Files\n\n```tsx\n// app/[[...slug]]/page.tsx\nexport { Page as default, generateMetadata, generateStaticParams } from \"@/lib/app\";\n```\n\n```tsx\n// app/[[...slug]]/layout.tsx\nimport { NextAppProvider } from \"@json-render/next\";\nimport { registry, handlers } from \"@/lib/registry\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <NextAppProvider registry={registry} handlers={handlers}>\n          {children}\n        </NextAppProvider>\n      </body>\n    </html>\n  );\n}\n```\n\n## Key Concepts\n\n### NextAppSpec\n\nThe top-level spec defines an entire Next.js application:\n\n- **metadata**: Root-level SEO metadata (title template, description, OpenGraph)\n- **layouts**: Reusable layout element trees (each must include a `Slot` component)\n- **routes**: Route definitions keyed by URL pattern\n- **state**: Global initial state shared across all routes\n\n### Route Patterns\n\nRoutes use Next.js URL conventions:\n\n- `\"/\"` -- home page\n- `\"/about\"` -- static route\n- `\"/blog/[slug]\"` -- dynamic segment\n- `\"/docs/[...path]\"` -- catch-all segment\n- `\"/settings/[[...path]]\"` -- optional catch-all segment\n\n### Layouts\n\nLayouts wrap page content. Every layout MUST include a `Slot` component where page content will be rendered. Layouts are defined once in `spec.layouts` and referenced by routes via the `layout` field.\n\n### Built-in Components\n\n- **Slot**: Placeholder in layouts where page content is rendered\n- **Link**: Client-side navigation link (wraps `next/link`)\n\n### Built-in Actions\n\n- **setState**: Update state value. Params: `{ statePath, value }`\n- **pushState**: Append to array. Params: `{ statePath, value, clearStatePath? }`\n- **removeState**: Remove from array by index. Params: `{ statePath, index }`\n- **navigate**: Client-side navigation. Params: `{ href }`\n\n### Data Loaders\n\nServer-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.\n\n```typescript\ncreateNextApp({\n  spec,\n  loaders: {\n    loadPost: async ({ slug }) => {\n      const post = await db.post.findUnique({ where: { slug } });\n      return { post };\n    },\n  },\n});\n```\n\n### SSR\n\nPages are server-rendered automatically. The `createNextApp` `Page` component is an async Server Component that:\n\n1. Matches the route from the spec\n2. Runs server-side data loaders\n3. Generates metadata\n4. Passes the resolved spec to the client renderer for hydration\n\n### Entry Points\n\n- `@json-render/next` -- Client components (NextAppProvider, PageRenderer, Link)\n- `@json-render/next/server` -- Server utilities (createNextApp, matchRoute, schema)\n\n## API Reference\n\n### Server Exports (`@json-render/next/server`)\n\n- `createNextApp(options)` -- Create Page, generateMetadata, generateStaticParams\n- `schema` -- Custom schema for Next.js apps (for AI catalog generation)\n- `matchRoute(spec, pathname)` -- Match a URL to a route spec\n- `resolveMetadata(spec, route)` -- Resolve metadata for a route\n- `slugToPath(slug)` -- Convert catch-all slug array to pathname\n- `collectStaticParams(spec)` -- Collect static params for all routes\n\n### Client Exports (`@json-render/next`)\n\n- `NextAppProvider` -- Context provider for registry and handlers\n- `PageRenderer` -- Renders a page spec with optional layout\n- `NextErrorBoundary` -- Error boundary component\n- `NextLoading` -- Loading state component\n- `NextNotFound` -- Not-found component\n- `Link` -- Built-in navigation component (wraps next/link)\n","frontmatter":{"name":"next","description":"Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications."},"isInternal":false,"tokens":1385,"sizeBytes":5625},{"name":"SKILL.md","path":"skills/react-email/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/react-email/SKILL.md","title":"react-email","category":"anthropic-skill","format":"markdown","content":"---\nname: react-email\ndescription: React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email.\nmetadata:\n  tags: react-email, email, json-render, html email, transactional email\n---\n\n# @json-render/react-email\n\nReact Email renderer that converts JSON specs into HTML or plain-text email output.\n\n## Quick Start\n\n```typescript\nimport { renderToHtml } from \"@json-render/react-email\";\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email\";\nimport { defineCatalog } from \"@json-render/core\";\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n\nconst spec = {\n  root: \"html-1\",\n  elements: {\n    \"html-1\": { type: \"Html\", props: { lang: \"en\", dir: \"ltr\" }, children: [\"head-1\", \"body-1\"] },\n    \"head-1\": { type: \"Head\", props: {}, children: [] },\n    \"body-1\": {\n      type: \"Body\",\n      props: { style: { backgroundColor: \"#f6f9fc\" } },\n      children: [\"container-1\"],\n    },\n    \"container-1\": {\n      type: \"Container\",\n      props: { style: { maxWidth: \"600px\", margin: \"0 auto\", padding: \"20px\" } },\n      children: [\"heading-1\", \"text-1\"],\n    },\n    \"heading-1\": { type: \"Heading\", props: { text: \"Welcome\" }, children: [] },\n    \"text-1\": { type: \"Text\", props: { text: \"Thanks for signing up.\" }, children: [] },\n  },\n};\n\nconst html = await renderToHtml(spec);\n```\n\n## Spec Structure (Element Tree)\n\nSame flat element tree as `@json-render/react`: `root` key plus `elements` map. Root must be `Html`; children of `Html` should be `Head` and `Body`. Use `Container` (e.g. max-width 600px) inside `Body` for client-safe layout.\n\n## Creating a Catalog and Registry\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToHtml } from \"@json-render/react-email\";\nimport { standardComponentDefinitions } from \"@json-render/react-email/catalog\";\nimport { Container, Heading, Text } from \"@react-email/components\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Alert: {\n      props: z.object({\n        message: z.string(),\n        variant: z.enum([\"info\", \"success\", \"warning\"]).nullable(),\n      }),\n      slots: [],\n      description: \"A highlighted message block\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Alert: ({ props }) => (\n      <Container style={{ padding: 16, backgroundColor: \"#eff6ff\", borderRadius: 8 }}>\n        <Text style={{ margin: 0 }}>{props.message}</Text>\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\n## Server-Side Render APIs\n\n| Function | Purpose |\n|----------|---------|\n| `renderToHtml(spec, options?)` | Render spec to HTML email string |\n| `renderToPlainText(spec, options?)` | Render spec to plain-text email string |\n\n`RenderOptions`: `registry`, `includeStandard` (default true), `state` (for `$state` / `$cond`).\n\n## Visibility and State\n\nSupports `visible` conditions, `$state`, `$cond`, repeat (`repeat.statePath`), nested repeat paths with `{ \"$item\": \"field\" }`, and the same expression syntax as `@json-render/react`. Use `state` in `RenderOptions` when rendering server-side so expressions resolve.\n\n## Server-Safe Import\n\nImport schema and catalog without React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email/server\";\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create type-safe component registry from catalog |\n| `Renderer` | Render spec in browser (e.g. preview); use with `JSONUIProvider` for state/actions |\n| `createRenderer` | Standalone renderer component with state/actions/validation |\n| `renderToHtml` | Server: spec to HTML string |\n| `renderToPlainText` | Server: spec to plain-text string |\n| `schema` | Email element schema |\n| `standardComponents` | Pre-built component implementations |\n| `standardComponentDefinitions` | Catalog definitions (Zod props) |\n\n## Sub-path Exports\n\n| Path | Purpose |\n|------|---------|\n| `@json-render/react-email` | Full package |\n| `@json-render/react-email/server` | Schema and catalog only (no React) |\n| `@json-render/react-email/catalog` | Standard component definitions and types |\n| `@json-render/react-email/render` | Render functions only |\n\n## Standard Components\n\nAll components accept a `style` prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.\n\n### Document structure\n\n| Component | Description |\n|-----------|-------------|\n| `Html` | Root wrapper (lang, dir). Children: Head, Body. |\n| `Head` | Email head section. |\n| `Body` | Body wrapper; use `style` for background. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Constrain width (e.g. max-width 600px). |\n| `Section` | Group content; table-based for compatibility. |\n| `Row` | Horizontal row. |\n| `Column` | Column in a Row; set width via style. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (as: h1–h6). |\n| `Text` | Body text. |\n| `Link` | Hyperlink (text, href). |\n| `Button` | CTA link styled as button (text, href). |\n| `Image` | Image from URL (src, alt, width, height). |\n| `Hr` | Horizontal rule. |\n\n### Utility\n\n| Component | Description |\n|-----------|-------------|\n| `Preview` | Inbox preview text (inside Html). |\n| `Markdown` | Markdown content as email-safe HTML. |\n\n## Email Best Practices\n\n- Keep width constrained (e.g. Container max-width 600px).\n- Use inline styles or React Email's style props; many clients strip `<style>` blocks.\n- Prefer table-based layout (Section, Row, Column) for broad client support.\n- Use absolute URLs for images; many clients block relative or cid: references in some contexts.\n- Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.\n","frontmatter":{"name":"react-email","description":"React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email.","metadata":{"tags":"react-email, email, json-render, html email, transactional email"}},"isInternal":false,"tokens":1525,"sizeBytes":6286},{"name":"SKILL.md","path":"skills/react-native/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/react-native/SKILL.md","title":"react-native","category":"anthropic-skill","format":"markdown","content":"---\nname: react-native\ndescription: React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile.\n---\n\n# @json-render/react-native\n\nReact Native renderer that converts JSON specs into native mobile component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-native/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/react-native/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/react-native\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Icon: {\n      props: z.object({ name: z.string(), size: z.number().nullable(), color: z.string().nullable() }),\n      slots: [],\n      description: \"Icon display\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{}}>\n          <Renderer spec={spec} registry={registry} />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Standard Components\n\n### Layout\n- `Container` - wrapper with padding, background, border radius\n- `Row` - horizontal flex layout with gap, alignment\n- `Column` - vertical flex layout with gap, alignment\n- `ScrollContainer` - scrollable area (vertical or horizontal)\n- `SafeArea` - safe area insets for notch/home indicator\n- `Pressable` - touchable wrapper that triggers actions on press\n- `Spacer` - fixed or flexible spacing\n- `Divider` - thin line separator\n\n### Content\n- `Heading` - heading text (levels 1-6)\n- `Paragraph` - body text\n- `Label` - small label text\n- `Image` - image display with sizing modes\n- `Avatar` - circular avatar image\n- `Badge` - small status badge\n- `Chip` - tag/chip for categories\n\n### Input\n- `Button` - pressable button with variants\n- `TextInput` - text input field\n- `Switch` - toggle switch\n- `Checkbox` - checkbox with label\n- `Slider` - range slider\n- `SearchBar` - search input\n\n### Feedback\n- `Spinner` - loading indicator\n- `ProgressBar` - progress indicator\n\n### Composite\n- `Card` - card container with optional header\n- `ListItem` - list row with title, subtitle, accessory\n- `Modal` - bottom sheet modal\n\n## Visibility Conditions\n\nUse `visible` on elements. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `[ cond1, cond2 ]` for AND.\n\n## Pressable + setState Pattern\n\nUse `Pressable` with the built-in `setState` action for interactive UIs like tab bars:\n\n```json\n{\n  \"type\": \"Pressable\",\n  \"props\": {\n    \"action\": \"setState\",\n    \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n  },\n  \"children\": [\"home-icon\", \"home-label\"]\n}\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- Nested lists use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" } } }` inside an enclosing repeat.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n\n```json\n{\n  \"type\": \"TextInput\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Built-in Actions\n\nThe `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:\n\n```json\n{ \"action\": \"setState\", \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched from components |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `schema` | React Native element tree schema |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `createStandardActionHandlers` | Create handlers for standard actions |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way state binding via `$bindState`/`$bindItem` |\n| `useStateBinding` | _(deprecated)_ Legacy two-way binding by path |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n","frontmatter":{"name":"react-native","description":"React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile."},"isInternal":false,"tokens":1557,"sizeBytes":6498},{"name":"SKILL.md","path":"skills/react-pdf/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/react-pdf/SKILL.md","title":"react-pdf","category":"anthropic-skill","format":"markdown","content":"---\nname: react-pdf\ndescription: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files.\n---\n\n# @json-render/react-pdf\n\nReact PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\n## Quick Start\n\n```typescript\nimport { renderToBuffer } from \"@json-render/react-pdf\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"doc\",\n  elements: {\n    doc: { type: \"Document\", props: { title: \"Invoice\" }, children: [\"page\"] },\n    page: {\n      type: \"Page\",\n      props: { size: \"A4\" },\n      children: [\"heading\", \"table\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoice #1234\", level: \"h1\" },\n      children: [],\n    },\n    table: {\n      type: \"Table\",\n      props: {\n        columns: [\n          { header: \"Item\", width: \"60%\" },\n          { header: \"Price\", width: \"40%\", align: \"right\" },\n        ],\n        rows: [\n          [\"Widget A\", \"$10.00\"],\n          [\"Widget B\", \"$25.00\"],\n        ],\n      },\n      children: [],\n    },\n  },\n};\n\nconst buffer = await renderToBuffer(spec);\n```\n\n## Render APIs\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from \"@json-render/react-pdf\";\n\n// In-memory buffer\nconst buffer = await renderToBuffer(spec);\n\n// Readable stream (pipe to HTTP response)\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\n// Direct to file\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nAll render functions accept an optional second argument: `{ registry?, state?, handlers? }`.\n\n## Standard Components\n\n| Component | Description |\n|-----------|-------------|\n| `Document` | Top-level PDF wrapper (must be root) |\n| `Page` | Page with size (A4, LETTER), orientation, margins |\n| `View` | Generic container (padding, margin, background, border) |\n| `Row`, `Column` | Flex layout with gap, align, justify |\n| `Heading` | h1-h4 heading text |\n| `Text` | Body text (fontSize, color, weight, alignment) |\n| `Image` | Image from URL or base64 |\n| `Link` | Hyperlink with text and href |\n| `Table` | Data table with typed columns and rows |\n| `List` | Ordered or unordered list |\n| `Divider` | Horizontal line separator |\n| `Spacer` | Empty vertical space |\n| `PageNumber` | Current page number and total pages |\n\n## Custom Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToBuffer } from \"@json-render/react-pdf\";\nimport { standardComponentDefinitions } from \"@json-render/react-pdf/catalog\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Badge: ({ props }) => (\n      <View style={{ backgroundColor: props.color ?? \"#e5e7eb\", padding: 4 }}>\n        <Text>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## External Store (Controlled Mode)\n\nPass a `StateStore` for full control over state:\n\nNested lists can set `repeat.statePath` to `{ \"$item\": \"field\" }` inside an enclosing repeat.\n\n```typescript\nimport { createStateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\nstore.set(\"/invoice/total\", 200);\n```\n\n## Server-Safe Import\n\nImport schema and catalog without pulling in React:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-pdf/server\";\n```\n","frontmatter":{"name":"react-pdf","description":"React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files."},"isInternal":false,"tokens":931,"sizeBytes":3827},{"name":"SKILL.md","path":"skills/react-three-fiber/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/react-three-fiber/SKILL.md","title":"react-three-fiber","category":"anthropic-skill","format":"markdown","content":"---\nname: react-three-fiber\ndescription: React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs.\n---\n\n# @json-render/react-three-fiber\n\nReact Three Fiber renderer for json-render. 19 built-in 3D components.\n\n## Two Entry Points\n\n| Entry Point | Exports | Use For |\n|-------------|---------|---------|\n| `@json-render/react-three-fiber/catalog` | `threeComponentDefinitions` | Catalog schemas (no R3F dependency, safe for server) |\n| `@json-render/react-three-fiber` | `threeComponents`, `ThreeRenderer`, `ThreeCanvas`, schemas | R3F implementations and renderer |\n\n## Usage Pattern\n\nPick the 3D components you need from the standard definitions:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { threeComponentDefinitions } from \"@json-render/react-three-fiber/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { threeComponents, ThreeCanvas } from \"@json-render/react-three-fiber\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Box: threeComponentDefinitions.Box,\n    Sphere: threeComponentDefinitions.Sphere,\n    AmbientLight: threeComponentDefinitions.AmbientLight,\n    DirectionalLight: threeComponentDefinitions.DirectionalLight,\n    OrbitControls: threeComponentDefinitions.OrbitControls,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Box: threeComponents.Box,\n    Sphere: threeComponents.Sphere,\n    AmbientLight: threeComponents.AmbientLight,\n    DirectionalLight: threeComponents.DirectionalLight,\n    OrbitControls: threeComponents.OrbitControls,\n  },\n});\n```\n\n## Rendering\n\n### ThreeCanvas (convenience wrapper)\n\n```tsx\n<ThreeCanvas\n  spec={spec}\n  registry={registry}\n  shadows\n  camera={{ position: [5, 5, 5], fov: 50 }}\n  style={{ width: \"100%\", height: \"100vh\" }}\n/>\n```\n\n### Manual Canvas setup\n\n```tsx\nimport { Canvas } from \"@react-three/fiber\";\nimport { ThreeRenderer } from \"@json-render/react-three-fiber\";\n\n<Canvas shadows>\n  <ThreeRenderer spec={spec} registry={registry}>\n    {/* Additional R3F elements */}\n  </ThreeRenderer>\n</Canvas>\n```\n\n## Available Components (19)\n\n### Primitives (7)\n- `Box` -- width, height, depth, material\n- `Sphere` -- radius, widthSegments, heightSegments, material\n- `Cylinder` -- radiusTop, radiusBottom, height, material\n- `Cone` -- radius, height, material\n- `Torus` -- radius, tube, material\n- `Plane` -- width, height, material\n- `Capsule` -- radius, length, material\n\nAll primitives share: `position`, `rotation`, `scale`, `castShadow`, `receiveShadow`, `material`.\n\n### Lights (4)\n- `AmbientLight` -- color, intensity\n- `DirectionalLight` -- position, color, intensity, castShadow\n- `PointLight` -- position, color, intensity, distance, decay\n- `SpotLight` -- position, color, intensity, angle, penumbra\n\n### Other (8)\n- `Group` -- container with position/rotation/scale, supports children\n- `Model` -- GLTF/GLB loader via url prop\n- `Environment` -- HDRI environment map (preset, background, blur, intensity)\n- `Fog` -- linear fog (color, near, far)\n- `GridHelper` -- reference grid (size, divisions, color)\n- `Text3D` -- SDF text (text, fontSize, color, anchorX, anchorY)\n- `PerspectiveCamera` -- camera (position, fov, near, far, makeDefault)\n- `OrbitControls` -- orbit controls (enableDamping, enableZoom, autoRotate)\n\n## Shared Schemas\n\nReusable Zod schemas for custom 3D catalog definitions:\n\n```typescript\nimport { vector3Schema, materialSchema, transformProps, shadowProps } from \"@json-render/react-three-fiber\";\nimport { z } from \"zod\";\n\n// Custom 3D component\nconst myComponentDef = {\n  props: z.object({\n    ...transformProps,\n    ...shadowProps,\n    material: materialSchema.nullable(),\n    myCustomProp: z.string(),\n  }),\n  description: \"My custom 3D component\",\n};\n```\n\n## Material Schema\n\n```typescript\nmaterialSchema = z.object({\n  color: z.string().nullable(),         // default \"#ffffff\"\n  metalness: z.number().nullable(),     // default 0\n  roughness: z.number().nullable(),     // default 1\n  emissive: z.string().nullable(),      // default \"#000000\"\n  emissiveIntensity: z.number().nullable(), // default 1\n  opacity: z.number().nullable(),       // default 1\n  transparent: z.boolean().nullable(),  // default false\n  wireframe: z.boolean().nullable(),    // default false\n});\n```\n\n## Spec Format\n\n3D specs use the standard json-render flat element format:\n\n```json\n{\n  \"root\": \"scene\",\n  \"elements\": {\n    \"scene\": {\n      \"type\": \"Group\",\n      \"props\": { \"position\": [0, 0, 0] },\n      \"children\": [\"light\", \"box\"]\n    },\n    \"light\": {\n      \"type\": \"AmbientLight\",\n      \"props\": { \"intensity\": 0.5 },\n      \"children\": []\n    },\n    \"box\": {\n      \"type\": \"Box\",\n      \"props\": {\n        \"position\": [0, 0.5, 0],\n        \"material\": { \"color\": \"#4488ff\", \"metalness\": 0.3, \"roughness\": 0.7 }\n      },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Dependencies\n\nPeer dependencies required:\n- `@react-three/fiber` >= 8.0.0\n- `@react-three/drei` >= 9.0.0\n- `three` >= 0.160.0\n- `react` ^19.0.0\n- `zod` ^4.0.0\n","frontmatter":{"name":"react-three-fiber","description":"React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs."},"isInternal":false,"tokens":1389,"sizeBytes":5301},{"name":"SKILL.md","path":"skills/react/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/react/SKILL.md","title":"react","category":"anthropic-skill","format":"markdown","content":"---\nname: react\ndescription: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.\n---\n\n# @json-render/react\n\nReact renderer that converts JSON specs into React component trees.\n\n## Quick Start\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => <div>{props.title}{children}</div>,\n  },\n});\n\nfunction App({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { z } from \"zod\";\n\n// Create catalog with props schemas\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n      description: \"Card container with title\",\n    },\n    Layout: {\n      props: z.object({}),\n      slots: [\"default\", \"header\", \"footer\"],\n      description: \"Layout with named content regions\",\n    },\n  },\n});\n\n// Define component implementations with type-safe props\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Button: ({ props }) => (\n      <button className={props.variant}>{props.label}</button>\n    ),\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h2>{props.title}</h2>\n        {children}\n      </div>\n    ),\n    Layout: ({ children, slots }) => (\n      <div>\n        <header>{slots?.header}</header>\n        <main>{children}</main>\n        <footer>{slots?.footer}</footer>\n      </div>\n    ),\n  },\n});\n```\n\n## Spec Structure (Element Tree)\n\nThe React schema uses an element tree format:\n\n```json\n{\n  \"root\": {\n    \"type\": \"Card\",\n    \"props\": { \"title\": \"Hello\" },\n    \"children\": [{ \"type\": \"Button\", \"props\": { \"label\": \"Click me\" } }]\n  }\n}\n```\n\n## Named Slots\n\nUse `children` for the `\"default\"` slot. Use the element's top-level `slots` object for other slot names declared by the catalog:\n\n```json\n{\n  \"type\": \"Layout\",\n  \"props\": {},\n  \"children\": [\"main\"],\n  \"slots\": {\n    \"header\": [\"heading\"],\n    \"footer\": [\"actions\"]\n  }\n}\n```\n\nRegistry components receive named content as `slots?.header`, `slots?.footer`, and so on. Do not use `slots.default`.\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. New syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR. Helpers: `visibility.when(\"/path\")`, `visibility.unless(\"/path\")`, `visibility.eq(\"/path\", val)`, `visibility.and(cond1, cond2)`, `visibility.or(cond1, cond2)`.\n\n## Providers\n\n| Provider             | Purpose                                                                                                |\n| -------------------- | ------------------------------------------------------------------------------------------------------ |\n| `StateProvider`      | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider`     | Handle actions dispatched via the event system                                                         |\n| `VisibilityProvider` | Enable conditional rendering based on state                                                            |\n| `ValidationProvider` | Form field validation                                                                                  |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>;\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved by the renderer before components receive props:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **Filtered lists**: `repeat` plus an `$item` visible condition on the same container renders only matching items: `{ \"repeat\": { \"statePath\": \"/tasks\", \"key\": \"id\" }, \"visible\": { \"$item\": \"status\", \"eq\": \"todo\" }, \"children\": [\"task-card\"] }`. AND-composed `$state` conjuncts gate the container shell; `$item`/`$index` conjuncts filter items.\n- **Nested lists**: inside a repeat, use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" }, \"key\": \"id\" } }` to iterate an array on the enclosing item.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n- **`{ \"$computed\": \"fn\", \"args\": { ... } }`** - calls registered functions with resolved args\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\nComponents receive already-resolved props. For two-way bound props, use the `useBoundProp` hook with the `bindings` map the renderer provides.\n\nRegister `$computed` functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:\n\n```tsx\n<JSONUIProvider\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n```\n\n## Event System\n\nComponents use `emit` to fire named events, or `on()` to get an event handle with metadata. The element's `on` field maps events to action bindings:\n\n```tsx\n// Simple event firing\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n),\n\n// Event handle with metadata (e.g. preventDefault)\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a href={props.href} onClick={(e) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    }}>{props.label}</a>\n  );\n},\n```\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } }\n}\n```\n\nThe `EventHandle` returned by `on()` has: `emit()`, `shouldPreventDefault` (boolean), and `bound` (boolean).\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\"]\n  },\n  \"watch\": { \"/form/country\": { \"action\": \"loadCities\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\nThe `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in catalog `actions`:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n{ \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } }\n```\n\n`validateForm` validates all registered fields and writes `{ valid, errors }` to state.\n\nNote: `statePath` in action params (e.g. `setState.statePath`) targets the mutation path. Two-way binding in component props uses `{ \"$bindState\": \"/path\" }` on the value prop, not `statePath`.\n\n## useBoundProp\n\nFor form components that need two-way binding, use `useBoundProp` with the `bindings` map the renderer provides when a prop uses `{ \"$bindState\": \"/path\" }` or `{ \"$bindItem\": \"field\" }`:\n\n```tsx\nimport { useBoundProp } from \"@json-render/react\";\n\nInput: ({ element, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(\n    element.props.value,\n    bindings?.value\n  );\n  return (\n    <input\n      value={value ?? \"\"}\n      onChange={(e) => setValue(e.target.value)}\n    />\n  );\n},\n```\n\n`useBoundProp(propValue, bindingPath)` returns `[value, setValue]`. The `value` is the resolved prop; `setValue` writes back to the bound state path (no-op if not bound).\n\n## BaseComponentProps\n\nFor building reusable component libraries not tied to a specific catalog (e.g. `@json-render/shadcn`):\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/react\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (\n  <div>{props.title}{children}</div>\n);\n```\n\n## defineRegistry\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it.\n\n## Key Exports\n\n| Export                  | Purpose                                                                                               |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `defineRegistry`        | Create a type-safe component registry from a catalog                                                  |\n| `Renderer`              | Render a spec using a registry                                                                        |\n| `schema`                | Element tree schema (includes built-in state actions: setState, pushState, removeState, validateForm) |\n| `useStateStore`         | Access state context                                                                                  |\n| `useStateValue`         | Get single value from state                                                                           |\n| `useBoundProp`          | Two-way binding for `$bindState`/`$bindItem` expressions                                              |\n| `useActions`            | Access actions context                                                                                |\n| `useAction`             | Get a single action dispatch function                                                                 |\n| `useOptionalValidation` | Non-throwing variant of useValidation (returns null if no provider)                                   |\n| `useUIStream`           | Stream specs from an API endpoint                                                                     |\n| `createStateStore`      | Create a framework-agnostic in-memory `StateStore`                                                    |\n| `StateStore`            | Interface for plugging in external state management                                                   |\n| `BaseComponentProps`    | Catalog-agnostic base type for reusable component libraries                                           |\n| `EventHandle`           | Event handle type (`emit`, `shouldPreventDefault`, `bound`)                                           |\n| `ComponentContext`      | Typed component context (catalog-aware)                                                               |\n","frontmatter":{"name":"react","description":"React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs."},"isInternal":false,"tokens":2653,"sizeBytes":11790},{"name":"SKILL.md","path":"skills/redux/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/redux/SKILL.md","title":"redux","category":"anthropic-skill","format":"markdown","content":"---\nname: redux\ndescription: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux.\n---\n\n# @json-render/redux\n\nRedux adapter for json-render's `StateStore` interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/redux @json-render/core @json-render/react redux\n# or with Redux Toolkit (recommended):\nnpm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit\n```\n\n## Usage\n\n```tsx\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Define a slice for json-render state\nconst uiSlice = createSlice({\n  name: \"ui\",\n  initialState: { count: 0 } as Record<string, unknown>,\n  reducers: {\n    replaceUiState: (_state, action) => action.payload,\n  },\n});\n\n// 2. Create the Redux store\nconst reduxStore = configureStore({\n  reducer: { ui: uiSlice.reducer },\n});\n\n// 3. Create the json-render StateStore adapter\nconst store = reduxStateStore({\n  store: reduxStore,\n  selector: (state) => state.ui,\n  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n});\n\n// 4. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Redux */}\n</StateProvider>\n```\n\n## API\n\n### `reduxStateStore(options)`\n\nCreates a `StateStore` backed by a Redux store.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `Store` | Yes | The Redux store instance |\n| `selector` | `(state) => StateModel` | Yes | Select the json-render slice from the Redux state tree. Use `(s) => s` if the entire state is the model. |\n| `dispatch` | `(nextState, store) => void` | Yes | Dispatch an action that replaces the selected slice with the next state |\n\nThe `dispatch` callback receives the full next state model and the Redux store.\n","frontmatter":{"name":"redux","description":"Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux."},"isInternal":false,"tokens":492,"sizeBytes":2023},{"name":"3d.md","path":"skills/remotion-best-practices/rules/3d.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/3d.md","title":"3d","category":"anthropic-skill","format":"markdown","content":"---\nname: 3d\ndescription: 3D content in Remotion using Three.js and React Three Fiber.\nmetadata:\n  tags: 3d, three, threejs\n---\n\n# Using Three.js and React Three Fiber in Remotion\n\nFollow React Three Fiber and Three.js best practices.  \nOnly the following Remotion-specific rules need to be followed:\n\n## Prerequisites\n\nFirst, the `@remotion/three` package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/three # If project uses npm\nbunx remotion add @remotion/three # If project uses bun\nyarn remotion add @remotion/three # If project uses yarn\npnpm exec remotion add @remotion/three # If project uses pnpm\n```\n\n## Using ThreeCanvas\n\nYou MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.  \n`<ThreeCanvas>` MUST have a `width` and `height` prop.\n\n```tsx\nimport { ThreeCanvas } from \"@remotion/three\";\nimport { useVideoConfig } from \"remotion\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <ambientLight intensity={0.4} />\n  <directionalLight position={[5, 5, 5]} intensity={0.8} />\n  <mesh>\n    <sphereGeometry args={[1, 32, 32]} />\n    <meshStandardMaterial color=\"red\" />\n  </mesh>\n</ThreeCanvas>\n```\n\n## No animations not driven by `useCurrentFrame()`\n\nShaders, models etc MUST NOT animate by themselves.  \nNo animations are allowed unless they are driven by `useCurrentFrame()`.  \nOtherwise, it will cause flickering during rendering.  \n\nUsing `useFrame()` from `@react-three/fiber` is forbidden.\n\n## Animate using `useCurrentFrame()`\n\nUse `useCurrentFrame()` to perform animations.\n\n```tsx\nconst frame = useCurrentFrame();\nconst rotationY = frame * 0.02;\n\n<mesh rotation={[0, rotationY, 0]}>\n  <boxGeometry args={[2, 2, 2]} />\n  <meshStandardMaterial color=\"#4a9eff\" />\n</mesh>\n```\n\n## Using `<Sequence>` inside `<ThreeCanvas>`\n\nThe `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.\n\n```tsx\nimport { Sequence } from \"remotion\";\nimport { ThreeCanvas } from \"@remotion/three\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <Sequence layout=\"none\">\n    <mesh>\n      <boxGeometry args={[2, 2, 2]} />\n      <meshStandardMaterial color=\"#4a9eff\" />\n    </mesh>\n  </Sequence>\n</ThreeCanvas>\n```","frontmatter":{"name":"3d","description":"3D content in Remotion using Three.js and React Three Fiber.","metadata":{"tags":"3d, three, threejs"}},"isInternal":false,"tokens":622,"sizeBytes":2290},{"name":"animations.md","path":"skills/remotion-best-practices/rules/animations.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/animations.md","title":"animations","category":"anthropic-skill","format":"markdown","content":"---\nname: animations\ndescription: Fundamental animation skills for Remotion\nmetadata:\n  tags: animations, transitions, frames, useCurrentFrame\n---\n\nAll animations MUST be driven by the `useCurrentFrame()` hook.  \nWrite animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.\n\n```tsx\nimport { useCurrentFrame } from \"remotion\";\n\nexport const FadeIn = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {\n    extrapolateRight: 'clamp',\n  });\n \n  return (\n    <div style={{ opacity }}>Hello World!</div>\n  );\n};\n```\n\nCSS transitions or animations are FORBIDDEN - they will not render correctly.  \nTailwind animation class names are FORBIDDEN - they will not render correctly.  ","frontmatter":{"name":"animations","description":"Fundamental animation skills for Remotion","metadata":{"tags":"animations, transitions, frames, useCurrentFrame"}},"isInternal":false,"tokens":191,"sizeBytes":790},{"name":"assets.md","path":"skills/remotion-best-practices/rules/assets.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/assets.md","title":"assets","category":"anthropic-skill","format":"markdown","content":"---\nname: assets\ndescription: Importing images, videos, audio, and fonts into Remotion\nmetadata:\n  tags: assets, staticFile, images, fonts, public\n---\n\n# Importing assets in Remotion\n\n## The public folder\n\nPlace assets in the `public/` folder at your project root.\n\n## Using staticFile()\n\nYou MUST use `staticFile()` to reference files from the `public/` folder:\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\nexport const MyComposition = () => {\n  return <Img src={staticFile('logo.png')} />;\n};\n```\n\nThe function returns an encoded URL that works correctly when deploying to subdirectories.\n\n## Using with components\n\n**Images:**\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\n<Img src={staticFile('photo.png')} />;\n```\n\n**Videos:**\n\n```tsx\nimport {Video} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Video src={staticFile('clip.mp4')} />;\n```\n\n**Audio:**\n\n```tsx\nimport {Audio} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Audio src={staticFile('music.mp3')} />;\n```\n\n**Fonts:**\n\n```tsx\nimport {staticFile} from 'remotion';\n\nconst fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);\nawait fontFamily.load();\ndocument.fonts.add(fontFamily);\n```\n\n## Remote URLs\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n<Video src=\"https://remotion.media/video.mp4\" />\n```\n\n## Important notes\n\n- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering\n- Special characters in filenames (`#`, `?`, `&`) are automatically encoded\n","frontmatter":{"name":"assets","description":"Importing images, videos, audio, and fonts into Remotion","metadata":{"tags":"assets, staticFile, images, fonts, public"}},"isInternal":false,"tokens":407,"sizeBytes":1590},{"name":"audio.md","path":"skills/remotion-best-practices/rules/audio.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/audio.md","title":"audio","category":"anthropic-skill","format":"markdown","content":"---\nname: audio\ndescription: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch\nmetadata:\n  tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx\n---\n\n# Using audio in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/media package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/media\n```\n\n## Importing Audio\n\nUse `<Audio>` from `@remotion/media` to add audio to your composition.\n\n```tsx\nimport { Audio } from \"@remotion/media\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Audio src={staticFile(\"audio.mp3\")} />;\n};\n```\n\nRemote URLs are also supported:\n\n```tsx\n<Audio src=\"https://remotion.media/audio.mp3\" />\n```\n\nBy default, audio plays from the start, at full volume and full length.\nMultiple audio tracks can be layered by adding multiple `<Audio>` components.\n\n## Trimming\n\nUse `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.\n\n```tsx\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    trimBefore={2 * fps} // Skip the first 2 seconds\n    trimAfter={10 * fps} // End at the 10 second mark\n  />\n);\n```\n\nThe audio still starts playing at the beginning of the composition - only the specified portion is played.\n\n## Delaying\n\nWrap the audio in a `<Sequence>` to delay when it starts:\n\n```tsx\nimport { Sequence, staticFile } from \"remotion\";\nimport { Audio } from \"@remotion/media\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Sequence from={1 * fps}>\n    <Audio src={staticFile(\"audio.mp3\")} />\n  </Sequence>\n);\n```\n\nThe audio will start playing after 1 second.\n\n## Volume\n\nSet a static volume (0 to 1):\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} volume={0.5} />\n```\n\nOr use a callback for dynamic volume based on the current frame:\n\n```tsx\nimport { interpolate } from \"remotion\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    volume={(f) =>\n      interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: \"clamp\" })\n    }\n  />\n);\n```\n\nThe value of `f` starts at 0 when the audio begins to play, not the composition frame.\n\n## Muting\n\nUse `muted` to silence the audio. It can be set dynamically:\n\n```tsx\nconst frame = useCurrentFrame();\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s\n  />\n);\n```\n\n## Speed\n\nUse `playbackRate` to change the playback speed:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={2} /> {/* 2x speed */}\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={0.5} /> {/* Half speed */}\n```\n\nReverse playback is not supported.\n\n## Looping\n\nUse `loop` to loop the audio indefinitely:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} loop />\n```\n\nUse `loopVolumeCurveBehavior` to control how the frame count behaves when looping:\n\n- `\"repeat\"`: Frame count resets to 0 each loop (default)\n- `\"extend\"`: Frame count continues incrementing\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  loop\n  loopVolumeCurveBehavior=\"extend\"\n  volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops\n/>\n```\n\n## Pitch\n\nUse `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={1.5} // Higher pitch\n/>\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={0.8} // Lower pitch\n/>\n```\n\nPitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.\n","frontmatter":{"name":"audio","description":"Using audio and sound in Remotion - importing, trimming, volume, speed, pitch","metadata":{"tags":"audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx"}},"isInternal":false,"tokens":975,"sizeBytes":3625},{"name":"calculate-metadata.md","path":"skills/remotion-best-practices/rules/calculate-metadata.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/calculate-metadata.md","title":"calculate-metadata","category":"anthropic-skill","format":"markdown","content":"---\nname: calculate-metadata\ndescription: Dynamically set composition duration, dimensions, and props\nmetadata:\n  tags: calculateMetadata, duration, dimensions, props, dynamic\n---\n\n# Using calculateMetadata\n\nUse `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.\n\n```tsx\n<Composition id=\"MyComp\" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />\n```\n\n## Setting duration based on a video\n\nUse the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:\n\n```tsx\nimport {CalculateMetadataFunction} from 'remotion';\nimport {getMediaMetadata} from '../get-media-metadata';\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n  };\n};\n```\n\n## Matching dimensions of a video\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n    width: dimensions?.width ?? 1920,\n    height: dimensions?.height ?? 1080,\n  };\n};\n```\n\n## Setting duration based on multiple videos\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));\n  const allMetadata = await Promise.all(metadataPromises);\n\n  const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);\n\n  return {\n    durationInFrames: Math.ceil(totalDuration * 30),\n  };\n};\n```\n\n## Setting a default outName\n\nSet the default output filename based on props:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  return {\n    defaultOutName: `video-${props.id}.mp4`,\n  };\n};\n```\n\n## Transforming props\n\nFetch data or transform props before rendering:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {\n  const response = await fetch(props.dataUrl, {signal: abortSignal});\n  const data = await response.json();\n\n  return {\n    props: {\n      ...props,\n      fetchedData: data,\n    },\n  };\n};\n```\n\nThe `abortSignal` cancels stale requests when props change in the Studio.\n\n## Return value\n\nAll fields are optional. Returned values override the `<Composition>` props:\n\n- `durationInFrames`: Number of frames\n- `width`: Composition width in pixels\n- `height`: Composition height in pixels\n- `fps`: Frames per second\n- `props`: Transformed props passed to the component\n- `defaultOutName`: Default output filename\n- `defaultCodec`: Default codec for rendering\n","frontmatter":{"name":"calculate-metadata","description":"Dynamically set composition duration, dimensions, and props","metadata":{"tags":"calculateMetadata, duration, dimensions, props, dynamic"}},"isInternal":false,"tokens":662,"sizeBytes":2920},{"name":"can-decode.md","path":"skills/remotion-best-practices/rules/can-decode.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/can-decode.md","title":"can-decode","category":"anthropic-skill","format":"markdown","content":"---\nname: can-decode\ndescription: Check if a video can be decoded by the browser using Mediabunny\nmetadata:\n  tags: decode, validation, video, audio, compatibility, browser\n---\n\n# Checking if a video can be decoded\n\nUse Mediabunny to check if a video can be decoded by the browser before attempting to play it.\n\n## The `canDecode()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const canDecode = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  try {\n    await input.getFormat();\n  } catch {\n    return false;\n  }\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (videoTrack && !(await videoTrack.canDecode())) {\n    return false;\n  }\n\n  const audioTrack = await input.getPrimaryAudioTrack();\n  if (audioTrack && !(await audioTrack.canDecode())) {\n    return false;\n  }\n\n  return true;\n};\n```\n\n## Usage\n\n```tsx\nconst src = \"https://remotion.media/video.mp4\";\nconst isDecodable = await canDecode(src);\n\nif (isDecodable) {\n  console.log(\"Video can be decoded\");\n} else {\n  console.log(\"Video cannot be decoded by this browser\");\n}\n```\n\n## Using with Blob\n\nFor file uploads or drag-and-drop, use `BlobSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, BlobSource } from \"mediabunny\";\n\nexport const canDecodeBlob = async (blob: Blob) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new BlobSource(blob),\n  });\n\n  // Same validation logic as above\n};\n```\n","frontmatter":{"name":"can-decode","description":"Check if a video can be decoded by the browser using Mediabunny","metadata":{"tags":"decode, validation, video, audio, compatibility, browser"}},"isInternal":false,"tokens":401,"sizeBytes":1587},{"name":"charts.md","path":"skills/remotion-best-practices/rules/charts.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/charts.md","title":"charts","category":"anthropic-skill","format":"markdown","content":"---\nname: charts\ndescription: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.\nmetadata:\n  tags: charts, data, visualization, bar-chart, pie-chart, graphs\n---\n\n# Charts in Remotion\n\nYou can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.\n\n## No animations not powered by `useCurrentFrame()`\n\nDisable all animations by third party libraries.  \nThey will cause flickering during rendering.  \nInstead, drive all animations from `useCurrentFrame()`.\n\n## Bar Chart Animations\n\nSee [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.\n\n### Staggered Bars\n\nYou can animate the height of the bars and stagger them like this:\n\n```tsx\nconst STAGGER_DELAY = 5;\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst bars = data.map((item, i) => {\n  const delay = i * STAGGER_DELAY;\n  const height = spring({\n    frame,\n    fps,\n    delay,\n    config: {damping: 200},\n  });\n  return <div style={{height: height * item.value}} />;\n});\n```\n\n## Pie Chart Animation\n\nAnimate segments using stroke-dashoffset, starting from 12 o'clock.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst progress = interpolate(frame, [0, 100], [0, 1]);\n\nconst circumference = 2 * Math.PI * radius;\nconst segmentLength = (value / total) * circumference;\nconst offset = interpolate(progress, [0, 1], [segmentLength, 0]);\n\n<circle r={radius} cx={center} cy={center} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;\n```\n","frontmatter":{"name":"charts","description":"Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.","metadata":{"tags":"charts, data, visualization, bar-chart, pie-chart, graphs"}},"isInternal":false,"tokens":432,"sizeBytes":1751},{"name":"compositions.md","path":"skills/remotion-best-practices/rules/compositions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/compositions.md","title":"compositions","category":"anthropic-skill","format":"markdown","content":"---\nname: compositions\ndescription: Defining compositions, stills, folders, default props and dynamic metadata\nmetadata:\n  tags: composition, still, folder, props, metadata\n---\n\nA `<Composition>` defines the component, width, height, fps and duration of a renderable video.\n\nIt normally is placed in the `src/Root.tsx` file.\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;\n};\n```\n\n## Default Props\n\nPass `defaultProps` to provide initial values for your component.  \nValues must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100}\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={\n        {\n          title: 'Hello World',\n          color: '#ff0000',\n        } satisfies MyCompositionProps\n      }\n    />\n  );\n};\n```\n\nUse `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.\n\n## Folders\n\nUse `<Folder>` to organize compositions in the sidebar.  \nFolder names can only contain letters, numbers, and hyphens.\n\n```tsx\nimport {Composition, Folder} from 'remotion';\n\nexport const RemotionRoot = () => {\n  return (\n    <>\n      <Folder name=\"Marketing\">\n        <Composition id=\"Promo\" /* ... */ />\n        <Composition id=\"Ad\" /* ... */ />\n      </Folder>\n      <Folder name=\"Social\">\n        <Folder name=\"Instagram\">\n          <Composition id=\"Story\" /* ... */ />\n          <Composition id=\"Reel\" /* ... */ />\n        </Folder>\n      </Folder>\n    </>\n  );\n};\n```\n\n## Stills\n\nUse `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.\n\n```tsx\nimport {Still} from 'remotion';\nimport {Thumbnail} from './Thumbnail';\n\nexport const RemotionRoot = () => {\n  return <Still id=\"Thumbnail\" component={Thumbnail} width={1280} height={720} />;\n};\n```\n\n## Calculate Metadata\n\nUse `calculateMetadata` to make dimensions, duration, or props dynamic based on data.\n\n```tsx\nimport {Composition, CalculateMetadataFunction} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nconst calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {\n  const data = await fetch(`https://api.example.com/video/${props.videoId}`, {\n    signal: abortSignal,\n  }).then((res) => res.json());\n\n  return {\n    durationInFrames: Math.ceil(data.duration * 30),\n    props: {\n      ...props,\n      videoUrl: data.url,\n    },\n  };\n};\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100} // Placeholder, will be overridden\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={{videoId: 'abc123'}}\n      calculateMetadata={calculateMetadata}\n    />\n  );\n};\n```\n\nThe function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","frontmatter":{"name":"compositions","description":"Defining compositions, stills, folders, default props and dynamic metadata","metadata":{"tags":"composition, still, folder, props, metadata"}},"isInternal":false,"tokens":882,"sizeBytes":3708},{"name":"display-captions.md","path":"skills/remotion-best-practices/rules/display-captions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/display-captions.md","title":"display-captions","category":"anthropic-skill","format":"markdown","content":"---\nname: display-captions\ndescription: Displaying captions in Remotion with TikTok-style pages and word highlighting\nmetadata:\n  tags: captions, subtitles, display, tiktok, highlight\n---\n\n# Displaying captions in Remotion\n\nThis guide explains how to display captions in Remotion, assuming you already have captions in the [`Caption`](https://www.remotion.dev/docs/captions/caption) format.\n\n## Prerequisites\n\nRead [Transcribing audio](transcribe-captions.md) for how to generate captions.\n\nFirst, the [`@remotion/captions`](https://www.remotion.dev/docs/captions) package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions\n```\n\n## Fetching captions\n\nFirst, fetch your captions JSON file. Use [`useDelayRender()`](https://www.remotion.dev/docs/use-delay-render) to hold the render until the captions are loaded:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      // Assuming captions.json is in the public/ folder.\n      const response = await fetch(staticFile(\"captions123.json\"));\n      const data = await response.json();\n      setCaptions(data);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;\n};\n```\n\n## Creating pages\n\nUse `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:\n\n```tsx\nimport { useMemo } from \"react\";\nimport { createTikTokStyleCaptions } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\n// How often captions should switch (in milliseconds)\n// Higher values = more words per page\n// Lower values = fewer words (more word-by-word)\nconst SWITCH_CAPTIONS_EVERY_MS = 1200;\n\nconst { pages } = useMemo(() => {\n  return createTikTokStyleCaptions({\n    captions,\n    combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,\n  });\n}, [captions]);\n```\n\n## Rendering with Sequences\n\nMap over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:\n\n```tsx\nimport { Sequence, useVideoConfig, AbsoluteFill } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst CaptionedContent: React.FC = () => {\n  const { fps } = useVideoConfig();\n\n  return (\n    <AbsoluteFill>\n      {pages.map((page, index) => {\n        const nextPage = pages[index + 1] ?? null;\n        const startFrame = (page.startMs / 1000) * fps;\n        const endFrame = Math.min(\n          nextPage ? (nextPage.startMs / 1000) * fps : Infinity,\n          startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,\n        );\n        const durationInFrames = endFrame - startFrame;\n\n        if (durationInFrames <= 0) {\n          return null;\n        }\n\n        return (\n          <Sequence\n            key={index}\n            from={startFrame}\n            durationInFrames={durationInFrames}\n          >\n            <CaptionPage page={page} />\n          </Sequence>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n```\n\n## White-space preservation\n\nThe captions are whitespace sensitive. You should include spaces in the `text` field before each word. Use `whiteSpace: \"pre\"` to preserve the whitespace in the captions.\n\n## Separate component for captions\n\nPut captioning logic in a separate component.  \nMake a new file for it.\n\n## Word highlighting\n\nA caption page contains `tokens` which you can use to highlight the currently spoken word:\n\n```tsx\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst HIGHLIGHT_COLOR = \"#39E508\";\n\nconst CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Current time relative to the start of the sequence\n  const currentTimeMs = (frame / fps) * 1000;\n  // Convert to absolute time by adding the page start\n  const absoluteTimeMs = page.startMs + currentTimeMs;\n\n  return (\n    <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n      <div style={{ fontSize: 80, fontWeight: \"bold\", whiteSpace: \"pre\" }}>\n        {page.tokens.map((token) => {\n          const isActive =\n            token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;\n\n          return (\n            <span\n              key={token.fromMs}\n              style={{ color: isActive ? HIGHLIGHT_COLOR : \"white\" }}\n            >\n              {token.text}\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n```\n\n## Display captions alongside video content\n\nBy default, put the captions alongside the video content, so the captions are in sync.  \nFor each video, make a new captions JSON file.\n\n```tsx\n<AbsoluteFill>\n  <Video src={staticFile(\"video.mp4\")} />\n  <CaptionPage page={page} />\n</AbsoluteFill>\n```\n","frontmatter":{"name":"display-captions","description":"Displaying captions in Remotion with TikTok-style pages and word highlighting","metadata":{"tags":"captions, subtitles, display, tiktok, highlight"}},"isInternal":false,"tokens":1303,"sizeBytes":5471},{"name":"extract-frames.md","path":"skills/remotion-best-practices/rules/extract-frames.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/extract-frames.md","title":"extract-frames","category":"anthropic-skill","format":"markdown","content":"---\nname: extract-frames\ndescription: Extract frames from videos at specific timestamps using Mediabunny\nmetadata:\n  tags: frames, extract, video, thumbnail, filmstrip, canvas\n---\n\n# Extracting frames from videos\n\nUse Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.\n\n## The `extractFrames()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport {\n  ALL_FORMATS,\n  Input,\n  UrlSource,\n  VideoSample,\n  VideoSampleSink,\n} from \"mediabunny\";\n\ntype Options = {\n  track: { width: number; height: number };\n  container: string;\n  durationInSeconds: number | null;\n};\n\nexport type ExtractFramesTimestampsInSecondsFn = (\n  options: Options\n) => Promise<number[]> | number[];\n\nexport type ExtractFramesProps = {\n  src: string;\n  timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;\n  onVideoSample: (sample: VideoSample) => void;\n  signal?: AbortSignal;\n};\n\nexport async function extractFrames({\n  src,\n  timestampsInSeconds,\n  onVideoSample,\n  signal,\n}: ExtractFramesProps): Promise<void> {\n  using input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src),\n  });\n\n  const [durationInSeconds, format, videoTrack] = await Promise.all([\n    input.computeDuration(),\n    input.getFormat(),\n    input.getPrimaryVideoTrack(),\n  ]);\n\n  if (!videoTrack) {\n    throw new Error(\"No video track found in the input\");\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const timestamps =\n    typeof timestampsInSeconds === \"function\"\n      ? await timestampsInSeconds({\n          track: {\n            width: videoTrack.displayWidth,\n            height: videoTrack.displayHeight,\n          },\n          container: format.name,\n          durationInSeconds,\n        })\n      : timestampsInSeconds;\n\n  if (timestamps.length === 0) {\n    return;\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const sink = new VideoSampleSink(videoTrack);\n\n  for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {\n    if (signal?.aborted) {\n      break;\n    }\n\n    if (!videoSample) {\n      continue;\n    }\n\n    onVideoSample(videoSample);\n  }\n}\n```\n\n## Basic usage\n\nExtract frames at specific timestamps:\n\n```tsx\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: [0, 1, 2, 3, 4],\n  onVideoSample: (sample) => {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Creating a filmstrip\n\nUse a callback function to dynamically calculate timestamps based on video metadata:\n\n```tsx\nconst canvasWidth = 500;\nconst canvasHeight = 80;\nconst fromSeconds = 0;\nconst toSeconds = 10;\n\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: async ({ track, durationInSeconds }) => {\n    const aspectRatio = track.width / track.height;\n    const amountOfFramesFit = Math.ceil(\n      canvasWidth / (canvasHeight * aspectRatio)\n    );\n    const segmentDuration = toSeconds - fromSeconds;\n    const timestamps: number[] = [];\n\n    for (let i = 0; i < amountOfFramesFit; i++) {\n      timestamps.push(\n        fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)\n      );\n    }\n\n    return timestamps;\n  },\n  onVideoSample: (sample) => {\n    console.log(`Frame at ${sample.timestamp}s`);\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Cancellation with AbortSignal\n\nCancel frame extraction after a timeout:\n\n```tsx\nconst controller = new AbortController();\n\nsetTimeout(() => controller.abort(), 5000);\n\ntry {\n  await extractFrames({\n    src: \"https://remotion.media/video.mp4\",\n    timestampsInSeconds: [0, 1, 2, 3, 4],\n    onVideoSample: (sample) => {\n      using frame = sample;\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = frame.displayWidth;\n      canvas.height = frame.displayHeight;\n      const ctx = canvas.getContext(\"2d\");\n      frame.draw(ctx!, 0, 0);\n    },\n    signal: controller.signal,\n  });\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n\n## Timeout with Promise.race\n\n```tsx\nconst controller = new AbortController();\n\nconst timeoutPromise = new Promise<never>((_, reject) => {\n  const timeoutId = setTimeout(() => {\n    controller.abort();\n    reject(new Error(\"Frame extraction timed out after 10 seconds\"));\n  }, 10000);\n\n  controller.signal.addEventListener(\"abort\", () => clearTimeout(timeoutId), {\n    once: true,\n  });\n});\n\ntry {\n  await Promise.race([\n    extractFrames({\n      src: \"https://remotion.media/video.mp4\",\n      timestampsInSeconds: [0, 1, 2, 3, 4],\n      onVideoSample: (sample) => {\n        using frame = sample;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = frame.displayWidth;\n        canvas.height = frame.displayHeight;\n        const ctx = canvas.getContext(\"2d\");\n        frame.draw(ctx!, 0, 0);\n      },\n      signal: controller.signal,\n    }),\n    timeoutPromise,\n  ]);\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n","frontmatter":{"name":"extract-frames","description":"Extract frames from videos at specific timestamps using Mediabunny","metadata":{"tags":"frames, extract, video, thumbnail, filmstrip, canvas"}},"isInternal":false,"tokens":1298,"sizeBytes":5496},{"name":"fonts.md","path":"skills/remotion-best-practices/rules/fonts.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/fonts.md","title":"fonts","category":"anthropic-skill","format":"markdown","content":"---\nname: fonts\ndescription: Loading Google Fonts and local fonts in Remotion\nmetadata:\n  tags: fonts, google-fonts, typography, text\n---\n\n# Using fonts in Remotion\n\n## Google Fonts with @remotion/google-fonts\n\nThe recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.\n\n### Prerequisites\n\nFirst, the @remotion/google-fonts package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/google-fonts # If project uses npm\nbunx remotion add @remotion/google-fonts # If project uses bun\nyarn remotion add @remotion/google-fonts # If project uses yarn\npnpm exec remotion add @remotion/google-fonts # If project uses pnpm\n```\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily } = loadFont();\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily }}>Hello World</div>;\n};\n```\n\nPreferrably, specify only needed weights and subsets to reduce file size:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Roboto\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n```\n\n### Waiting for font to load\n\nUse `waitUntilDone()` if you need to know when the font is ready:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Lobster\";\n\nconst { fontFamily, waitUntilDone } = loadFont();\n\nawait waitUntilDone();\n```\n\n## Local fonts with @remotion/fonts\n\nFor local font files, use the `@remotion/fonts` package.\n\n### Prerequisites\n\nFirst, install @remotion/fonts:\n\n```bash\nnpx remotion add @remotion/fonts # If project uses npm\nbunx remotion add @remotion/fonts # If project uses bun\nyarn remotion add @remotion/fonts # If project uses yarn\npnpm exec remotion add @remotion/fonts # If project uses pnpm\n```\n\n### Loading a local font\n\nPlace your font file in the `public/` folder and use `loadFont()`:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait loadFont({\n  family: \"MyFont\",\n  url: staticFile(\"MyFont-Regular.woff2\"),\n});\n\nexport const MyComposition = () => {\n  return <div style={{ fontFamily: \"MyFont\" }}>Hello World</div>;\n};\n```\n\n### Loading multiple weights\n\nLoad each weight separately with the same family name:\n\n```tsx\nimport { loadFont } from \"@remotion/fonts\";\nimport { staticFile } from \"remotion\";\n\nawait Promise.all([\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Regular.woff2\"),\n    weight: \"400\",\n  }),\n  loadFont({\n    family: \"Inter\",\n    url: staticFile(\"Inter-Bold.woff2\"),\n    weight: \"700\",\n  }),\n]);\n```\n\n### Available options\n\n```tsx\nloadFont({\n  family: \"MyFont\", // Required: name to use in CSS\n  url: staticFile(\"font.woff2\"), // Required: font file URL\n  format: \"woff2\", // Optional: auto-detected from extension\n  weight: \"400\", // Optional: font weight\n  style: \"normal\", // Optional: normal or italic\n  display: \"block\", // Optional: font-display behavior\n});\n```\n\n## Using in components\n\nCall `loadFont()` at the top level of your component or in a separate file that's imported early:\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Montserrat\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"400\", \"700\"],\n  subsets: [\"latin\"],\n});\n\nexport const Title: React.FC<{ text: string }> = ({ text }) => {\n  return (\n    <h1\n      style={{\n        fontFamily,\n        fontSize: 80,\n        fontWeight: \"bold\",\n      }}\n    >\n      {text}\n    </h1>\n  );\n};\n```\n","frontmatter":{"name":"fonts","description":"Loading Google Fonts and local fonts in Remotion","metadata":{"tags":"fonts, google-fonts, typography, text"}},"isInternal":false,"tokens":901,"sizeBytes":3484},{"name":"get-audio-duration.md","path":"skills/remotion-best-practices/rules/get-audio-duration.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/get-audio-duration.md","title":"get-audio-duration","category":"anthropic-skill","format":"markdown","content":"---\nname: get-audio-duration\ndescription: Getting the duration of an audio file in seconds with Mediabunny\nmetadata:\n  tags: duration, audio, length, time, seconds, mp3, wav\n---\n\n# Getting audio duration with Mediabunny\n\nMediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.\n\n## Getting audio duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getAudioDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getAudioDuration(\"https://remotion.media/audio.mp3\");\nconsole.log(duration); // e.g. 180.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getAudioDuration(staticFile(\"audio.mp3\"));\n```\n","frontmatter":{"name":"get-audio-duration","description":"Getting the duration of an audio file in seconds with Mediabunny","metadata":{"tags":"duration, audio, length, time, seconds, mp3, wav"}},"isInternal":false,"tokens":334,"sizeBytes":1354},{"name":"get-video-dimensions.md","path":"skills/remotion-best-practices/rules/get-video-dimensions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/get-video-dimensions.md","title":"get-video-dimensions","category":"anthropic-skill","format":"markdown","content":"---\nname: get-video-dimensions\ndescription: Getting the width and height of a video file with Mediabunny\nmetadata:\n  tags: dimensions, width, height, resolution, size, video\n---\n\n# Getting video dimensions with Mediabunny\n\nMediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video dimensions\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDimensions = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (!videoTrack) {\n    throw new Error(\"No video track found\");\n  }\n\n  return {\n    width: videoTrack.displayWidth,\n    height: videoTrack.displayHeight,\n  };\n};\n```\n\n## Usage\n\n```tsx\nconst dimensions = await getVideoDimensions(\"https://remotion.media/video.mp4\");\nconsole.log(dimensions.width);  // e.g. 1920\nconsole.log(dimensions.height); // e.g. 1080\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst videoTrack = await input.getPrimaryVideoTrack();\nconst width = videoTrack.displayWidth;\nconst height = videoTrack.displayHeight;\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst dimensions = await getVideoDimensions(staticFile(\"video.mp4\"));\n```\n","frontmatter":{"name":"get-video-dimensions","description":"Getting the width and height of a video file with Mediabunny","metadata":{"tags":"dimensions, width, height, resolution, size, video"}},"isInternal":false,"tokens":403,"sizeBytes":1627},{"name":"get-video-duration.md","path":"skills/remotion-best-practices/rules/get-video-duration.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/get-video-duration.md","title":"get-video-duration","category":"anthropic-skill","format":"markdown","content":"---\nname: get-video-duration\ndescription: Getting the duration of a video file in seconds with Mediabunny\nmetadata:\n  tags: duration, video, length, time, seconds\n---\n\n# Getting video duration with Mediabunny\n\nMediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.\n\n## Getting video duration\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const getVideoDuration = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  const durationInSeconds = await input.computeDuration();\n  return durationInSeconds;\n};\n```\n\n## Usage\n\n```tsx\nconst duration = await getVideoDuration(\"https://remotion.media/video.mp4\");\nconsole.log(duration); // e.g. 10.5 (seconds)\n```\n\n## Using with local files\n\nFor local files, use `FileSource` instead of `UrlSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, FileSource } from \"mediabunny\";\n\nconst input = new Input({\n  formats: ALL_FORMATS,\n  source: new FileSource(file), // File object from input or drag-drop\n});\n\nconst durationInSeconds = await input.computeDuration();\n```\n\n## Using with staticFile in Remotion\n\n```tsx\nimport { staticFile } from \"remotion\";\n\nconst duration = await getVideoDuration(staticFile(\"video.mp4\"));\n```\n","frontmatter":{"name":"get-video-duration","description":"Getting the duration of a video file in seconds with Mediabunny","metadata":{"tags":"duration, video, length, time, seconds"}},"isInternal":false,"tokens":328,"sizeBytes":1341},{"name":"gifs.md","path":"skills/remotion-best-practices/rules/gifs.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/gifs.md","title":"gif","category":"anthropic-skill","format":"markdown","content":"---\nname: gif\ndescription: Displaying GIFs, APNG, AVIF and WebP in Remotion\nmetadata:\n  tags: gif, animation, images, animated, apng, avif, webp\n---\n\n# Using Animated images in Remotion\n\n## Basic usage\n\nUse `<AnimatedImage>` to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:\n\n```tsx\nimport { AnimatedImage, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return (\n    <AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} />\n  );\n};\n```\n\nRemote URLs are also supported (must have CORS enabled):\n\n```tsx\n<AnimatedImage\n  src=\"https://example.com/animation.gif\"\n  width={500}\n  height={500}\n/>\n```\n\n## Sizing and fit\n\nControl how the image fills its container with the `fit` prop:\n\n```tsx\n// Stretch to fill (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"fill\" />\n\n// Maintain aspect ratio, fit inside container\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"contain\" />\n\n// Fill container, crop if needed\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={300} fit=\"cover\" />\n```\n\n## Playback speed\n\nUse `playbackRate` to control the animation speed:\n\n```tsx\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}\n```\n\n## Looping behavior\n\nControl what happens when the animation finishes:\n\n```tsx\n// Loop indefinitely (default)\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"loop\" />\n\n// Play once, show final frame\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"pause-after-finish\" />\n\n// Play once, then clear canvas\n<AnimatedImage src={staticFile(\"animation.gif\")} width={500} height={500} loopBehavior=\"clear-after-finish\" />\n```\n\n## Styling\n\nUse the `style` prop for additional CSS (use `width` and `height` props for sizing):\n\n```tsx\n<AnimatedImage\n  src={staticFile(\"animation.gif\")}\n  width={500}\n  height={500}\n  style={{\n    borderRadius: 20,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n  }}\n/>\n```\n\n## Getting GIF duration\n\nUse `getGifDurationInSeconds()` from `@remotion/gif` to get the duration of a GIF.\n\n```bash\nnpx remotion add @remotion/gif\n```\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nconst duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\nconsole.log(duration); // e.g. 2.5\n```\n\nThis is useful for setting the composition duration to match the GIF:\n\n```tsx\nimport { getGifDurationInSeconds } from \"@remotion/gif\";\nimport { staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const duration = await getGifDurationInSeconds(staticFile(\"animation.gif\"));\n  return {\n    durationInFrames: Math.ceil(duration * 30),\n  };\n};\n```\n\n## Alternative\n\nIf `<AnimatedImage>` does not work (only supported in Chrome and Firefox), you can use `<Gif>` from `@remotion/gif` instead.\n\n```bash\nnpx remotion add @remotion/gif # If project uses npm\nbunx remotion add @remotion/gif # If project uses bun\nyarn remotion add @remotion/gif # If project uses yarn\npnpm exec remotion add @remotion/gif # If project uses pnpm\n```\n\n```tsx\nimport { Gif } from \"@remotion/gif\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Gif src={staticFile(\"animation.gif\")} width={500} height={500} />;\n};\n```\n\nThe `<Gif>` component has the same props as `<AnimatedImage>` but only supports GIF files.\n","frontmatter":{"name":"gif","description":"Displaying GIFs, APNG, AVIF and WebP in Remotion","metadata":{"tags":"gif, animation, images, animated, apng, avif, webp"}},"isInternal":false,"tokens":962,"sizeBytes":3707},{"name":"images.md","path":"skills/remotion-best-practices/rules/images.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/images.md","title":"images","category":"anthropic-skill","format":"markdown","content":"---\nname: images\ndescription: Embedding images in Remotion using the <Img> component\nmetadata:\n  tags: images, img, staticFile, png, jpg, svg, webp\n---\n\n# Using images in Remotion\n\n## The `<Img>` component\n\nAlways use the `<Img>` component from `remotion` to display images:\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Img src={staticFile(\"photo.png\")} />;\n};\n```\n\n## Important restrictions\n\n**You MUST use the `<Img>` component from `remotion`.** Do not use:\n\n- Native HTML `<img>` elements\n- Next.js `<Image>` component\n- CSS `background-image`\n\nThe `<Img>` component ensures images are fully loaded before rendering, preventing flickering and blank frames during video export.\n\n## Local images with staticFile()\n\nPlace images in the `public/` folder and use `staticFile()` to reference them:\n\n```\nmy-video/\n├─ public/\n│  ├─ logo.png\n│  ├─ avatar.jpg\n│  └─ icon.svg\n├─ src/\n├─ package.json\n```\n\n```tsx\nimport { Img, staticFile } from \"remotion\";\n\n<Img src={staticFile(\"logo.png\")} />\n```\n\n## Remote images\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n```\n\nEnsure remote images have CORS enabled.\n\nFor animated GIFs, use the `<Gif>` component from `@remotion/gif` instead.\n\n## Sizing and positioning\n\nUse the `style` prop to control size and position:\n\n```tsx\n<Img\n  src={staticFile(\"photo.png\")}\n  style={{\n    width: 500,\n    height: 300,\n    position: \"absolute\",\n    top: 100,\n    left: 50,\n    objectFit: \"cover\",\n  }}\n/>\n```\n\n## Dynamic image paths\n\nUse template literals for dynamic file references:\n\n```tsx\nimport { Img, staticFile, useCurrentFrame } from \"remotion\";\n\nconst frame = useCurrentFrame();\n\n// Image sequence\n<Img src={staticFile(`frames/frame${frame}.png`)} />\n\n// Selecting based on props\n<Img src={staticFile(`avatars/${props.userId}.png`)} />\n\n// Conditional images\n<Img src={staticFile(`icons/${isActive ? \"active\" : \"inactive\"}.svg`)} />\n```\n\nThis pattern is useful for:\n\n- Image sequences (frame-by-frame animations)\n- User-specific avatars or profile images\n- Theme-based icons\n- State-dependent graphics\n\n## Getting image dimensions\n\nUse `getImageDimensions()` to get the dimensions of an image:\n\n```tsx\nimport { getImageDimensions, staticFile } from \"remotion\";\n\nconst { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n```\n\nThis is useful for calculating aspect ratios or sizing compositions:\n\n```tsx\nimport { getImageDimensions, staticFile, CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction = async () => {\n  const { width, height } = await getImageDimensions(staticFile(\"photo.png\"));\n  return {\n    width,\n    height,\n  };\n};\n```\n","frontmatter":{"name":"images","description":"Embedding images in Remotion using the <Img> component","metadata":{"tags":"images, img, staticFile, png, jpg, svg, webp"}},"isInternal":false,"tokens":688,"sizeBytes":2783},{"name":"import-srt-captions.md","path":"skills/remotion-best-practices/rules/import-srt-captions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/import-srt-captions.md","title":"import-srt-captions","category":"anthropic-skill","format":"markdown","content":"---\nname: import-srt-captions\ndescription: Importing .srt subtitle files into Remotion using @remotion/captions\nmetadata:\n  tags: captions, subtitles, srt, import, parse\n---\n\n# Importing .srt subtitles into Remotion\n\nIf you have an existing `.srt` subtitle file, you can import it into Remotion using `parseSrt()` from `@remotion/captions`.\n\nIf you don't have a .srt file, read [Transcribing audio](transcribe-captions.md) for how to generate captions instead.\n\n## Prerequisites\n\nFirst, the @remotion/captions package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions # If project uses npm\nbunx remotion add @remotion/captions # If project uses bun\nyarn remotion add @remotion/captions # If project uses yarn\npnpm exec remotion add @remotion/captions # If project uses pnpm\n```\n\n## Reading an .srt file\n\nUse `staticFile()` to reference an `.srt` file in your `public` folder, then fetch and parse it:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport { parseSrt } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      const response = await fetch(staticFile(\"subtitles.srt\"));\n      const text = await response.text();\n      const { captions: parsed } = parseSrt({ input: text });\n      setCaptions(parsed);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;\n};\n```\n\nRemote URLs are also supported - you can `fetch()` a remote file via URL instead of using `staticFile()`.\n\n## Using imported captions\n\nOnce parsed, the captions are in the `Caption` format and can be used with all `@remotion/captions` utilities.\n","frontmatter":{"name":"import-srt-captions","description":"Importing .srt subtitle files into Remotion using @remotion/captions","metadata":{"tags":"captions, subtitles, srt, import, parse"}},"isInternal":false,"tokens":575,"sizeBytes":2247},{"name":"light-leaks.md","path":"skills/remotion-best-practices/rules/light-leaks.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/light-leaks.md","title":"light-leaks","category":"anthropic-skill","format":"markdown","content":"---\nname: light-leaks\ndescription: Light leak overlay effects for Remotion using @remotion/light-leaks.\nmetadata:\n  tags: light-leaks, overlays, effects, transitions\n---\n\n## Light Leaks\n\nThis only works from Remotion 4.0.415 and up. Use `npx remotion versions` to check your Remotion version and `npx remotion upgrade` to upgrade your Remotion version.\n\n`<LightLeak>` from `@remotion/light-leaks` renders a WebGL-based light leak effect. It reveals during the first half of its duration and retracts during the second half.\n\nTypically used inside a `<TransitionSeries.Overlay>` to play over the cut point between two scenes. See the **transitions** rule for `<TransitionSeries>` and overlay usage.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/light-leaks\n```\n\n## Basic usage with TransitionSeries\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Props\n\n- `durationInFrames?` — defaults to the parent sequence/composition duration. The effect reveals during the first half and retracts during the second half.\n- `seed?` — determines the shape of the light leak pattern. Different seeds produce different patterns. Default: `0`.\n- `hueShift?` — rotates the hue in degrees (`0`–`360`). Default: `0` (yellow-to-orange). `120` = green, `240` = blue.\n\n## Customizing the look\n\n```tsx\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n// Blue-tinted light leak with a different pattern\n<LightLeak seed={5} hueShift={240} />;\n\n// Green-tinted light leak\n<LightLeak seed={2} hueShift={120} />;\n```\n\n## Standalone usage\n\n`<LightLeak>` can also be used outside of `<TransitionSeries>`, for example as a decorative overlay in any composition:\n\n```tsx\nimport { AbsoluteFill } from \"remotion\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\nconst MyComp: React.FC = () => (\n  <AbsoluteFill>\n    <MyContent />\n    <LightLeak durationInFrames={60} seed={3} />\n  </AbsoluteFill>\n);\n```\n","frontmatter":{"name":"light-leaks","description":"Light leak overlay effects for Remotion using @remotion/light-leaks.","metadata":{"tags":"light-leaks, overlays, effects, transitions"}},"isInternal":false,"tokens":581,"sizeBytes":2305},{"name":"lottie.md","path":"skills/remotion-best-practices/rules/lottie.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/lottie.md","title":"lottie","category":"anthropic-skill","format":"markdown","content":"---\nname: lottie\ndescription: Embedding Lottie animations in Remotion.\nmetadata:\n  category: Animation\n---\n\n# Using Lottie Animations in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/lottie package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/lottie # If project uses npm\nbunx remotion add @remotion/lottie # If project uses bun\nyarn remotion add @remotion/lottie # If project uses yarn\npnpm exec remotion add @remotion/lottie # If project uses pnpm\n```\n\n## Displaying a Lottie file\n\nTo import a Lottie animation:\n\n- Fetch the Lottie asset\n- Wrap the loading process in `delayRender()` and `continueRender()`\n- Save the animation data in a state\n- Render the Lottie animation using the `Lottie` component from the `@remotion/lottie` package\n\n```tsx\nimport {Lottie, LottieAnimationData} from '@remotion/lottie';\nimport {useEffect, useState} from 'react';\nimport {cancelRender, continueRender, delayRender} from 'remotion';\n\nexport const MyAnimation = () => {\n  const [handle] = useState(() => delayRender('Loading Lottie animation'));\n\n  const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);\n\n  useEffect(() => {\n    fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')\n      .then((data) => data.json())\n      .then((json) => {\n        setAnimationData(json);\n        continueRender(handle);\n      })\n      .catch((err) => {\n        cancelRender(err);\n      });\n  }, [handle]);\n\n  if (!animationData) {\n    return null;\n  }\n\n  return <Lottie animationData={animationData} />;\n};\n```\n\n## Styling and animating\n\nLottie supports the `style` prop to allow styles and animations:\n\n```tsx\nreturn <Lottie animationData={animationData} style={{width: 400, height: 400}} />;\n```\n\n","frontmatter":{"name":"lottie","description":"Embedding Lottie animations in Remotion.","metadata":{"category":"Animation"}},"isInternal":false,"tokens":455,"sizeBytes":1779},{"name":"maps.md","path":"skills/remotion-best-practices/rules/maps.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/maps.md","title":"maps","category":"anthropic-skill","format":"markdown","content":"---\nname: maps\ndescription: Make map animations with Mapbox\nmetadata:\n  tags: map, map animation, mapbox\n---\n\nMaps can be added to a Remotion video with Mapbox.  \nThe [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/api/) has the API reference.\n\n## Prerequisites\n\nMapbox and `@turf/turf` need to be installed.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `bun.lock` is found, use the following command:\n\n```bash\nbun i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i mapbox-gl @turf/turf @types/mapbox-gl\n```\n\nThe user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.\n\nThe mapbox token needs to be added to the `.env` file:\n\n```txt title=\".env\"\nREMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token\n```\n\n## Adding a map\n\nHere is a basic example of a map in Remotion.\n\n```tsx\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {AbsoluteFill, useDelayRender, useVideoConfig} from 'remotion';\nimport mapboxgl, {Map} from 'mapbox-gl';\n\nexport const lineCoordinates = [\n  [6.56158447265625, 46.059891147620725],\n  [6.5691375732421875, 46.05679376154153],\n  [6.5842437744140625, 46.05059898938315],\n  [6.594886779785156, 46.04702502069337],\n  [6.601066589355469, 46.0460718554722],\n  [6.6089630126953125, 46.0365370783104],\n  [6.6185760498046875, 46.018420689207964],\n];\n\nmapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;\n\nexport const MyComposition = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const {delayRender, continueRender} = useDelayRender();\n\n  const {width, height} = useVideoConfig();\n  const [handle] = useState(() => delayRender('Loading map...'));\n  const [map, setMap] = useState<Map | null>(null);\n\n  useEffect(() => {\n    const _map = new Map({\n      container: ref.current!,\n      zoom: 11.53,\n      center: [6.5615, 46.0598],\n      pitch: 65,\n      bearing: 0,\n      style: '⁠mapbox://styles/mapbox/standard',\n      interactive: false,\n      fadeDuration: 0,\n    });\n\n    _map.on('style.load', () => {\n      // Hide all features from the Mapbox Standard style\n      const hideFeatures = [\n        'showRoadsAndTransit',\n        'showRoads',\n        'showTransit',\n        'showPedestrianRoads',\n        'showRoadLabels',\n        'showTransitLabels',\n        'showPlaceLabels',\n        'showPointOfInterestLabels',\n        'showPointsOfInterest',\n        'showAdminBoundaries',\n        'showLandmarkIcons',\n        'showLandmarkIconLabels',\n        'show3dObjects',\n        'show3dBuildings',\n        'show3dTrees',\n        'show3dLandmarks',\n        'show3dFacades',\n      ];\n      for (const feature of hideFeatures) {\n        _map.setConfigProperty('basemap', feature, false);\n      }\n\n      _map.setConfigProperty('basemap', 'colorTrunks', 'rgba(0, 0, 0, 0)');\n\n      _map.addSource('trace', {\n        type: 'geojson',\n        data: {\n          type: 'Feature',\n          properties: {},\n          geometry: {\n            type: 'LineString',\n            coordinates: lineCoordinates,\n          },\n        },\n      });\n      _map.addLayer({\n        type: 'line',\n        source: 'trace',\n        id: 'line',\n        paint: {\n          'line-color': 'black',\n          'line-width': 5,\n        },\n        layout: {\n          'line-cap': 'round',\n          'line-join': 'round',\n        },\n      });\n    });\n\n    _map.on('load', () => {\n      continueRender(handle);\n      setMap(_map);\n    });\n  }, [handle, lineCoordinates]);\n\n  const style: React.CSSProperties = useMemo(() => ({width, height, position: 'absolute'}), [width, height]);\n\n  return <AbsoluteFill ref={ref} style={style} />;\n};\n```\n\nThe following is important in Remotion:\n\n- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.\n- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.\n- The element containing the ref MUST have an explicit width and height and `position: \"absolute\"`.\n- Do not add a `_map.remove();` cleanup function.\n\n## Drawing lines\n\nUnless I request it, do not add a glow effect to the lines.\nUnless I request it, do not add additional points to the lines.\n\n## Map style\n\nBy default, use the `mapbox://styles/mapbox/standard` style.  \nHide the labels from the base map style.\n\nUnless I request otherwise, remove all features from the Mapbox Standard style.\n\n```tsx\n// Hide all features from the Mapbox Standard style\nconst hideFeatures = [\n  'showRoadsAndTransit',\n  'showRoads',\n  'showTransit',\n  'showPedestrianRoads',\n  'showRoadLabels',\n  'showTransitLabels',\n  'showPlaceLabels',\n  'showPointOfInterestLabels',\n  'showPointsOfInterest',\n  'showAdminBoundaries',\n  'showLandmarkIcons',\n  'showLandmarkIconLabels',\n  'show3dObjects',\n  'show3dBuildings',\n  'show3dTrees',\n  'show3dLandmarks',\n  'show3dFacades',\n];\nfor (const feature of hideFeatures) {\n  _map.setConfigProperty('basemap', feature, false);\n}\n\n_map.setConfigProperty('basemap', 'colorMotorways', 'transparent');\n_map.setConfigProperty('basemap', 'colorRoads', 'transparent');\n_map.setConfigProperty('basemap', 'colorTrunks', 'transparent');\n```\n\n## Animating the camera\n\nYou can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.\n\nUnless I ask for it, do not jump between camera angles.\n\n```tsx\nimport * as turf from '@turf/turf';\nimport {interpolate} from 'remotion';\nimport {Easing} from 'remotion';\nimport {useCurrentFrame, useVideoConfig, useDelayRender} from 'remotion';\n\nconst animationDuration = 20;\nconst cameraAltitude = 4000;\n```\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\nconst {delayRender, continueRender} = useDelayRender();\n\nuseEffect(() => {\n  if (!map) {\n    return;\n  }\n  const handle = delayRender('Moving point...');\n\n  const routeDistance = turf.length(turf.lineString(lineCoordinates));\n\n  const progress = interpolate(frame / fps, [0.00001, animationDuration], [0, 1], {\n    easing: Easing.inOut(Easing.sin),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  const camera = map.getFreeCameraOptions();\n\n  const alongRoute = turf.along(turf.lineString(lineCoordinates), routeDistance * progress).geometry.coordinates;\n\n  camera.lookAtPoint({\n    lng: alongRoute[0],\n    lat: alongRoute[1],\n  });\n\n  map.setFreeCameraOptions(camera);\n  map.once('idle', () => continueRender(handle));\n}, [lineCoordinates, fps, frame, handle, map]);\n```\n\nNotes:\n\nIMPORTANT: Keep the camera by default so north is up.\nIMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.\n\n- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors\n- See [Timing](./timing.md) for more options for timing.\n- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.\n\n## Animating lines\n\n### Straight lines (linear interpolation)\n\nTo animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {durationInFrames} = useVideoConfig();\n\nuseEffect(() => {\n  if (!map) return;\n\n  const animationHandle = delayRender('Animating line...');\n\n  const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.inOut(Easing.cubic),\n  });\n\n  // Linear interpolation for a straight line on the map\n  const start = lineCoordinates[0];\n  const end = lineCoordinates[1];\n  const currentLng = start[0] + (end[0] - start[0]) * progress;\n  const currentLat = start[1] + (end[1] - start[1]) * progress;\n\n  const lineData: GeoJSON.Feature<GeoJSON.LineString> = {\n    type: 'Feature',\n    properties: {},\n    geometry: {\n      type: 'LineString',\n      coordinates: [start, [currentLng, currentLat]],\n    },\n  };\n\n  const source = map.getSource('trace') as mapboxgl.GeoJSONSource;\n  if (source) {\n    source.setData(lineData);\n  }\n\n  map.once('idle', () => continueRender(animationHandle));\n}, [frame, map, durationInFrames]);\n```\n\n### Curved lines (geodesic/great circle)\n\nTo animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.\n\n```tsx\nimport * as turf from '@turf/turf';\n\nconst routeLine = turf.lineString(lineCoordinates);\nconst routeDistance = turf.length(routeLine);\n\nconst currentDistance = Math.max(0.001, routeDistance * progress);\nconst slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);\n\nconst source = map.getSource('route') as mapboxgl.GeoJSONSource;\nif (source) {\n  source.setData(slicedLine);\n}\n```\n\n## Markers\n\nAdd labels, and markers where appropriate.\n\n```tsx\n_map.addSource('markers', {\n  type: 'geojson',\n  data: {\n    type: 'FeatureCollection',\n    features: [\n      {\n        type: 'Feature',\n        properties: {name: 'Point 1'},\n        geometry: {type: 'Point', coordinates: [-118.2437, 34.0522]},\n      },\n    ],\n  },\n});\n\n_map.addLayer({\n  id: 'city-markers',\n  type: 'circle',\n  source: 'markers',\n  paint: {\n    'circle-radius': 40,\n    'circle-color': '#FF4444',\n    'circle-stroke-width': 4,\n    'circle-stroke-color': '#FFFFFF',\n  },\n});\n\n_map.addLayer({\n  id: 'labels',\n  type: 'symbol',\n  source: 'markers',\n  layout: {\n    'text-field': ['get', 'name'],\n    'text-font': ['DIN Pro Bold', 'Arial Unicode MS Bold'],\n    'text-size': 50,\n    'text-offset': [0, 0.5],\n    'text-anchor': 'top',\n  },\n  paint: {\n    'text-color': '#FFFFFF',\n    'text-halo-color': '#000000',\n    'text-halo-width': 2,\n  },\n});\n```\n\nMake sure they are big enough. Check the composition dimensions and scale the labels accordingly.\nFor a composition size of 1920x1080, the label font size should be at least 40px.\n\nIMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:\n\n```tsx\n\"text-offset\": [0, 0.5],\n```\n\n## 3D buildings\n\nTo enable 3D buildings, use the following code:\n\n```tsx\n_map.setConfigProperty('basemap', 'show3dObjects', true);\n_map.setConfigProperty('basemap', 'show3dLandmarks', true);\n_map.setConfigProperty('basemap', 'show3dBuildings', true);\n```\n\n## Rendering\n\nWhen rendering a map animation, make sure to render with the following flags:\n\n```\nnpx remotion render --gl=angle --concurrency=1\n```\n","frontmatter":{"name":"maps","description":"Make map animations with Mapbox","metadata":{"tags":"map, map animation, mapbox"}},"isInternal":false,"tokens":2967,"sizeBytes":11217},{"name":"measuring-dom-nodes.md","path":"skills/remotion-best-practices/rules/measuring-dom-nodes.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/measuring-dom-nodes.md","title":"measuring-dom-nodes","category":"anthropic-skill","format":"markdown","content":"---\nname: measuring-dom-nodes\ndescription: Measuring DOM element dimensions in Remotion\nmetadata:\n  tags: measure, layout, dimensions, getBoundingClientRect, scale\n---\n\n# Measuring DOM nodes in Remotion\n\nRemotion applies a `scale()` transform to the video container, which affects values from `getBoundingClientRect()`. Use `useCurrentScale()` to get correct measurements.\n\n## Measuring element dimensions\n\n```tsx\nimport { useCurrentScale } from \"remotion\";\nimport { useRef, useEffect, useState } from \"react\";\n\nexport const MyComponent = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const scale = useCurrentScale();\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    if (!ref.current) return;\n    const rect = ref.current.getBoundingClientRect();\n    setDimensions({\n      width: rect.width / scale,\n      height: rect.height / scale,\n    });\n  }, [scale]);\n\n  return <div ref={ref}>Content to measure</div>;\n};\n```\n\n","frontmatter":{"name":"measuring-dom-nodes","description":"Measuring DOM element dimensions in Remotion","metadata":{"tags":"measure, layout, dimensions, getBoundingClientRect, scale"}},"isInternal":false,"tokens":222,"sizeBytes":974},{"name":"measuring-text.md","path":"skills/remotion-best-practices/rules/measuring-text.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/measuring-text.md","title":"measuring-text","category":"anthropic-skill","format":"markdown","content":"---\nname: measuring-text\ndescription: Measuring text dimensions, fitting text to containers, and checking overflow\nmetadata:\n  tags: measure, text, layout, dimensions, fitText, fillTextBox\n---\n\n# Measuring text in Remotion\n\n## Prerequisites\n\nInstall @remotion/layout-utils if it is not already installed:\n\n```bash\nnpx remotion add @remotion/layout-utils # If project uses npm\nbunx remotion add @remotion/layout-utils # If project uses bun\nyarn remotion add @remotion/layout-utils # If project uses yarn\npnpm exec remotion add @remotion/layout-utils # If project uses pnpm\n```\n\n## Measuring text dimensions\n\nUse `measureText()` to calculate the width and height of text:\n\n```tsx\nimport { measureText } from \"@remotion/layout-utils\";\n\nconst { width, height } = measureText({\n  text: \"Hello World\",\n  fontFamily: \"Arial\",\n  fontSize: 32,\n  fontWeight: \"bold\",\n});\n```\n\nResults are cached - duplicate calls return the cached result.\n\n## Fitting text to a width\n\nUse `fitText()` to find the optimal font size for a container:\n\n```tsx\nimport { fitText } from \"@remotion/layout-utils\";\n\nconst { fontSize } = fitText({\n  text: \"Hello World\",\n  withinWidth: 600,\n  fontFamily: \"Inter\",\n  fontWeight: \"bold\",\n});\n\nreturn (\n  <div\n    style={{\n      fontSize: Math.min(fontSize, 80), // Cap at 80px\n      fontFamily: \"Inter\",\n      fontWeight: \"bold\",\n    }}\n  >\n    Hello World\n  </div>\n);\n```\n\n## Checking text overflow\n\nUse `fillTextBox()` to check if text exceeds a box:\n\n```tsx\nimport { fillTextBox } from \"@remotion/layout-utils\";\n\nconst box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });\n\nconst words = [\"Hello\", \"World\", \"This\", \"is\", \"a\", \"test\"];\nfor (const word of words) {\n  const { exceedsBox } = box.add({\n    text: word + \" \",\n    fontFamily: \"Arial\",\n    fontSize: 24,\n  });\n  if (exceedsBox) {\n    // Text would overflow, handle accordingly\n    break;\n  }\n}\n```\n\n## Best practices\n\n**Load fonts first:** Only call measurement functions after fonts are loaded.\n\n```tsx\nimport { loadFont } from \"@remotion/google-fonts/Inter\";\n\nconst { fontFamily, waitUntilDone } = loadFont(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nwaitUntilDone().then(() => {\n  // Now safe to measure\n  const { width } = measureText({\n    text: \"Hello\",\n    fontFamily,\n    fontSize: 32,\n  });\n})\n```\n\n**Use validateFontIsLoaded:** Catch font loading issues early:\n\n```tsx\nmeasureText({\n  text: \"Hello\",\n  fontFamily: \"MyCustomFont\",\n  fontSize: 32,\n  validateFontIsLoaded: true, // Throws if font not loaded\n});\n```\n\n**Match font properties:** Use the same properties for measurement and rendering:\n\n```tsx\nconst fontStyle = {\n  fontFamily: \"Inter\",\n  fontSize: 32,\n  fontWeight: \"bold\" as const,\n  letterSpacing: \"0.5px\",\n};\n\nconst { width } = measureText({\n  text: \"Hello\",\n  ...fontStyle,\n});\n\nreturn <div style={fontStyle}>Hello</div>;\n```\n\n**Avoid padding and border:** Use `outline` instead of `border` to prevent layout differences:\n\n```tsx\n<div style={{ outline: \"2px solid red\" }}>Text</div>\n```\n","frontmatter":{"name":"measuring-text","description":"Measuring text dimensions, fitting text to containers, and checking overflow","metadata":{"tags":"measure, text, layout, dimensions, fitText, fillTextBox"}},"isInternal":false,"tokens":779,"sizeBytes":3000},{"name":"parameters.md","path":"skills/remotion-best-practices/rules/parameters.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/parameters.md","title":"parameters","category":"anthropic-skill","format":"markdown","content":"---\nname: parameters\ndescription: Make a video parametrizable by adding a Zod schema\nmetadata:\n  tags: parameters, zod, schema\n---\n\nTo make a video parametrizable, a Zod schema can be added to a composition.\n\nFirst, `zod` must be installed - it must be exactly version `3.22.3`.\n\nSearch the project for lockfiles and run the correct command depending on the package manager:\n\nIf `package-lock.json` is found, use the following command:\n\n```bash\nnpm i zod@3.22.3\n```\n\nIf `bun.lockb` is found, use the following command:\n\n```bash\nbun i zod@3.22.3\n```\n\nIf `yarn.lock` is found, use the following command:\n\n```bash\nyarn add zod@3.22.3\n```\n\nIf `pnpm-lock.yaml` is found, use the following command:\n\n```bash\npnpm i zod@3.22.3\n```\n\nThen, a Zod schema can be defined alongside the component:\n\n```tsx title=\"src/MyComposition.tsx\"\nimport {z} from 'zod';\n\nexport const MyCompositionSchema = z.object({\n  title: z.string(),\n});\n\nconst MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => {\n  return (\n    <div>\n      <h1>{props.title}</h1>\n    </div>\n  );\n};\n```\n\nIn the root file, the schema can be passed to the composition:\n\n```tsx title=\"src/Root.tsx\"\nimport {Composition} from 'remotion';\nimport {MycComponent, MyCompositionSchema} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{title: 'Hello World'}} schema={MyCompositionSchema} />;\n};\n```\n\nNow, the user can edit the parameter visually in the sidebar.\n\nAll schemas that are supported by Zod are supported by Remotion.\n\nRemotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.\n\n## Color picker\n\nFor adding a color picker, use `zColor()` from `@remotion/zod-types`.\n\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/zod-types # If project uses npm\nbunx remotion add @remotion/zod-types # If project uses bun\nyarn remotion add @remotion/zod-types # If project uses yarn\npnpm exec remotion add @remotion/zod-types # If project uses pnpm\n```\n\nThen import `zColor` from `@remotion/zod-types`:\n\n```tsx\nimport {zColor} from '@remotion/zod-types';\n```\n\nThen use it in the schema:\n\n```tsx\nexport const MyCompositionSchema = z.object({\n  color: zColor(),\n});\n```\n","frontmatter":{"name":"parameters","description":"Make a video parametrizable by adding a Zod schema","metadata":{"tags":"parameters, zod, schema"}},"isInternal":false,"tokens":647,"sizeBytes":2380},{"name":"sequencing.md","path":"skills/remotion-best-practices/rules/sequencing.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/sequencing.md","title":"sequencing","category":"anthropic-skill","format":"markdown","content":"---\nname: sequencing\ndescription: Sequencing patterns for Remotion - delay, trim, limit duration of items\nmetadata:\n  tags: sequence, series, timing, delay, trim\n---\n\nUse `<Sequence>` to delay when an element appears in the timeline.\n\n```tsx\nimport { Sequence } from \"remotion\";\n\nconst {fps} = useVideoConfig();\n\n<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Title />\n</Sequence>\n<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>\n  <Subtitle />\n</Sequence>\n```\n\nThis will by default wrap the component in an absolute fill element.  \nIf the items should not be wrapped, use the `layout` prop:\n\n```tsx\n<Sequence layout=\"none\">\n  <Title />\n</Sequence>\n```\n\n## Premounting\n\nThis loads the component in the timeline before it is actually played.  \nAlways premount any `<Sequence>`!\n\n```tsx\n<Sequence premountFor={1 * fps}>\n  <Title />\n</Sequence>\n```\n\n## Series\n\nUse `<Series>` when elements should play one after another without overlap.\n\n```tsx\nimport {Series} from 'remotion';\n\n<Series>\n  <Series.Sequence durationInFrames={45}>\n    <Intro />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={60}>\n    <MainContent />\n  </Series.Sequence>\n  <Series.Sequence durationInFrames={30}>\n    <Outro />\n  </Series.Sequence>\n</Series>;\n```\n\nSame as with `<Sequence>`, the items will be wrapped in an absolute fill element by default when using `<Series.Sequence>`, unless the `layout` prop is set to `none`.\n\n### Series with overlaps\n\nUse negative offset for overlapping sequences:\n\n```tsx\n<Series>\n  <Series.Sequence durationInFrames={60}>\n    <SceneA />\n  </Series.Sequence>\n  <Series.Sequence offset={-15} durationInFrames={60}>\n    {/* Starts 15 frames before SceneA ends */}\n    <SceneB />\n  </Series.Sequence>\n</Series>\n```\n\n## Frame References Inside Sequences\n\nInside a Sequence, `useCurrentFrame()` returns the local frame (starting from 0):\n\n```tsx\n<Sequence from={60} durationInFrames={30}>\n  <MyComponent />\n  {/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}\n</Sequence>\n```\n\n## Nested Sequences\n\nSequences can be nested for complex timing:\n\n```tsx\n<Sequence from={0} durationInFrames={120}>\n  <Background />\n  <Sequence from={15} durationInFrames={90} layout=\"none\">\n    <Title />\n  </Sequence>\n  <Sequence from={45} durationInFrames={60} layout=\"none\">\n    <Subtitle />\n  </Sequence>\n</Sequence>\n```\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n","frontmatter":{"name":"sequencing","description":"Sequencing patterns for Remotion - delay, trim, limit duration of items","metadata":{"tags":"sequence, series, timing, delay, trim"}},"isInternal":false,"tokens":686,"sizeBytes":2744},{"name":"subtitles.md","path":"skills/remotion-best-practices/rules/subtitles.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/subtitles.md","title":"subtitles","category":"anthropic-skill","format":"markdown","content":"---\nname: subtitles\ndescription: subtitles and caption rules\nmetadata:\n  tags: subtitles, captions, remotion, json\n---\n\nAll captions must be processed in JSON. The captions must use the `Caption` type which is the following:\n\n```ts\nimport type { Caption } from \"@remotion/captions\";\n```\n\nThis is the definition:\n\n```ts\ntype Caption = {\n  text: string;\n  startMs: number;\n  endMs: number;\n  timestampMs: number | null;\n  confidence: number | null;\n};\n```\n\n## Generating captions\n\nTo transcribe video and audio files to generate captions, load the [./transcribe-captions.md](./transcribe-captions.md) file for more instructions.\n\n## Displaying captions\n\nTo display captions in your video, load the [./display-captions.md](./display-captions.md) file for more instructions.\n\n## Importing captions\n\nTo import captions from a .srt file, load the [./import-srt-captions.md](./import-srt-captions.md) file for more instructions.\n","frontmatter":{"name":"subtitles","description":"subtitles and caption rules","metadata":{"tags":"subtitles, captions, remotion, json"}},"isInternal":false,"tokens":228,"sizeBytes":922},{"name":"tailwind.md","path":"skills/remotion-best-practices/rules/tailwind.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/tailwind.md","title":"tailwind","category":"anthropic-skill","format":"markdown","content":"---\nname: tailwind\ndescription: Using TailwindCSS in Remotion.\nmetadata:\n---\n\nYou can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.\n\nDon't use `transition-*` or `animate-*` classes - always animate using the `useCurrentFrame()` hook.  \n\nTailwind must be installed and enabled first in a Remotion project - fetch  https://www.remotion.dev/docs/tailwind using WebFetch for instructions.","frontmatter":{"name":"tailwind","description":"Using TailwindCSS in Remotion.","metadata":null},"isInternal":false,"tokens":99,"sizeBytes":422},{"name":"text-animations.md","path":"skills/remotion-best-practices/rules/text-animations.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/text-animations.md","title":"text-animations","category":"anthropic-skill","format":"markdown","content":"---\nname: text-animations\ndescription: Typography and text animation patterns for Remotion.\nmetadata:\n  tags: typography, text, typewriter, highlighter ken\n---\n\n## Text animations\n\nBased on `useCurrentFrame()`, reduce the string character by character to create a typewriter effect.\n\n## Typewriter Effect\n\nSee [Typewriter](assets/text-animations-typewriter.tsx) for an advanced example with a blinking cursor and a pause after the first sentence.\n\nAlways use string slicing for typewriter effects. Never use per-character opacity.\n\n## Word Highlighting\n\nSee [Word Highlight](assets/text-animations-word-highlight.tsx) for an example for how a word highlight is animated, like with a highlighter pen.\n","frontmatter":{"name":"text-animations","description":"Typography and text animation patterns for Remotion.","metadata":{"tags":"typography, text, typewriter, highlighter ken"}},"isInternal":false,"tokens":148,"sizeBytes":700},{"name":"timing.md","path":"skills/remotion-best-practices/rules/timing.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/timing.md","title":"timing","category":"anthropic-skill","format":"markdown","content":"---\nname: timing\ndescription: Interpolation curves in Remotion - linear, easing, spring animations\nmetadata:\n  tags: spring, bounce, easing, interpolation\n---\n\nA simple linear interpolation is done using the `interpolate` function.\n\n```ts title=\"Going from 0 to 1 over 100 frames\"\nimport {interpolate} from 'remotion';\n\nconst opacity = interpolate(frame, [0, 100], [0, 1]);\n```\n\nBy default, the values are not clamped, so the value can go outside the range [0, 1].  \nHere is how they can be clamped:\n\n```ts title=\"Going from 0 to 1 over 100 frames with extrapolation\"\nconst opacity = interpolate(frame, [0, 100], [0, 1], {\n  extrapolateRight: 'clamp',\n  extrapolateLeft: 'clamp',\n});\n```\n\n## Spring animations\n\nSpring animations have a more natural motion.  \nThey go from 0 to 1 over time.\n\n```ts title=\"Spring animation from 0 to 1 over 100 frames\"\nimport {spring, useCurrentFrame, useVideoConfig} from 'remotion';\n\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst scale = spring({\n  frame,\n  fps,\n});\n```\n\n### Physical properties\n\nThe default configuration is: `mass: 1, damping: 10, stiffness: 100`.  \nThis leads to the animation having a bit of bounce before it settles.\n\nThe config can be overwritten like this:\n\n```ts\nconst scale = spring({\n  frame,\n  fps,\n  config: {damping: 200},\n});\n```\n\nThe recommended configuration for a natural motion without a bounce is: `{ damping: 200 }`.\n\nHere are some common configurations:\n\n```tsx\nconst smooth = {damping: 200}; // Smooth, no bounce (subtle reveals)\nconst snappy = {damping: 20, stiffness: 200}; // Snappy, minimal bounce (UI elements)\nconst bouncy = {damping: 8}; // Bouncy entrance (playful animations)\nconst heavy = {damping: 15, stiffness: 80, mass: 2}; // Heavy, slow, small bounce\n```\n\n### Delay\n\nThe animation starts immediately by default.  \nUse the `delay` parameter to delay the animation by a number of frames.\n\n```tsx\nconst entrance = spring({\n  frame: frame - ENTRANCE_DELAY,\n  fps,\n  delay: 20,\n});\n```\n\n### Duration\n\nA `spring()` has a natural duration based on the physical properties.  \nTo stretch the animation to a specific duration, use the `durationInFrames` parameter.\n\n```tsx\nconst spring = spring({\n  frame,\n  fps,\n  durationInFrames: 40,\n});\n```\n\n### Combining spring() with interpolate()\n\nMap spring output (0-1) to custom ranges:\n\n```tsx\nconst springProgress = spring({\n  frame,\n  fps,\n});\n\n// Map to rotation\nconst rotation = interpolate(springProgress, [0, 1], [0, 360]);\n\n<div style={{rotate: rotation + 'deg'}} />;\n```\n\n### Adding springs\n\nSprings return just numbers, so math can be performed:\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps, durationInFrames} = useVideoConfig();\n\nconst inAnimation = spring({\n  frame,\n  fps,\n});\nconst outAnimation = spring({\n  frame,\n  fps,\n  durationInFrames: 1 * fps,\n  delay: durationInFrames - 1 * fps,\n});\n\nconst scale = inAnimation - outAnimation;\n```\n\n## Easing\n\nEasing can be added to the `interpolate` function:\n\n```ts\nimport {interpolate, Easing} from 'remotion';\n\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nThe default easing is `Easing.linear`.  \nThere are various other convexities:\n\n- `Easing.in` for starting slow and accelerating\n- `Easing.out` for starting fast and slowing down\n- `Easing.inOut`\n\nand curves (sorted from most linear to most curved):\n\n- `Easing.quad`\n- `Easing.sin`\n- `Easing.exp`\n- `Easing.circle`\n\nConvexities and curves need be combined for an easing function:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.inOut(Easing.quad),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n\nCubic bezier curves are also supported:\n\n```ts\nconst value1 = interpolate(frame, [0, 100], [0, 1], {\n  easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),\n  extrapolateLeft: 'clamp',\n  extrapolateRight: 'clamp',\n});\n```\n","frontmatter":{"name":"timing","description":"Interpolation curves in Remotion - linear, easing, spring animations","metadata":{"tags":"spring, bounce, easing, interpolation"}},"isInternal":false,"tokens":1076,"sizeBytes":3939},{"name":"transcribe-captions.md","path":"skills/remotion-best-practices/rules/transcribe-captions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/transcribe-captions.md","title":"transcribe-captions","category":"anthropic-skill","format":"markdown","content":"---\nname: transcribe-captions\ndescription: Transcribing audio to generate captions in Remotion\nmetadata:\n  tags: captions, transcribe, whisper, audio, speech-to-text\n---\n\n# Transcribing audio\n\nTo transcribe audio to generate captions in Remotion, you can use the [`transcribe()`](https://www.remotion.dev/docs/install-whisper-cpp/transcribe) function from the [`@remotion/install-whisper-cpp`](https://www.remotion.dev/docs/install-whisper-cpp) package.\n\n## Prerequisites\n\nFirst, the @remotion/install-whisper-cpp package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/install-whisper-cpp\n```\n\n## Transcribing\n\nMake a Node.js script to download Whisper.cpp and a model, and transcribe the audio.\n\n```ts\nimport path from \"path\";\nimport {\n  downloadWhisperModel,\n  installWhisperCpp,\n  transcribe,\n  toCaptions,\n} from \"@remotion/install-whisper-cpp\";\nimport fs from \"fs\";\n\nconst to = path.join(process.cwd(), \"whisper.cpp\");\n\nawait installWhisperCpp({\n  to,\n  version: \"1.5.5\",\n});\n\nawait downloadWhisperModel({\n  model: \"medium.en\",\n  folder: to,\n});\n\n// Convert the audio to a 16KHz wav file first if needed:\n// import {execSync} from 'child_process';\n// execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y');\n\nconst whisperCppOutput = await transcribe({\n  model: \"medium.en\",\n  whisperPath: to,\n  whisperCppVersion: \"1.5.5\",\n  inputPath: \"/path/to/audio123.wav\",\n  tokenLevelTimestamps: true,\n});\n\n// Optional: Apply our recommended postprocessing\nconst { captions } = toCaptions({\n  whisperCppOutput,\n});\n\n// Write it to the public/ folder so it can be fetched from Remotion\nfs.writeFileSync(\"captions123.json\", JSON.stringify(captions, null, 2));\n```\n\nTranscribe each clip individually and create multiple JSON files.\n\nSee [Displaying captions](display-captions.md) for how to display the captions in Remotion.\n","frontmatter":{"name":"transcribe-captions","description":"Transcribing audio to generate captions in Remotion","metadata":{"tags":"captions, transcribe, whisper, audio, speech-to-text"}},"isInternal":false,"tokens":485,"sizeBytes":1897},{"name":"transitions.md","path":"skills/remotion-best-practices/rules/transitions.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/transitions.md","title":"transitions","category":"anthropic-skill","format":"markdown","content":"---\nname: transitions\ndescription: Scene transitions and overlays for Remotion using TransitionSeries.\nmetadata:\n  tags: transitions, overlays, fade, slide, wipe, scenes\n---\n\n## TransitionSeries\n\n`<TransitionSeries>` arranges scenes and supports two ways to enhance the cut point between them:\n\n- **Transitions** (`<TransitionSeries.Transition>`) — crossfade, slide, wipe, etc. between two scenes. Shortens the timeline because both scenes play simultaneously during the transition.\n- **Overlays** (`<TransitionSeries.Overlay>`) — render an effect (e.g. a light leak) on top of the cut point without shortening the timeline.\n\nChildren are absolutely positioned.\n\n## Prerequisites\n\n```bash\nnpx remotion add @remotion/transitions\n```\n\n## Transition example\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Overlay example\n\nAny React component can be used as an overlay. For a ready-made effect, see the **light-leaks** rule.\n\n```tsx\nimport { TransitionSeries } from \"@remotion/transitions\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={20}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Mixing transitions and overlays\n\nTransitions and overlays can coexist in the same `<TransitionSeries>`, but an overlay cannot be adjacent to a transition or another overlay.\n\n```tsx\nimport { TransitionSeries, linearTiming } from \"@remotion/transitions\";\nimport { fade } from \"@remotion/transitions/fade\";\nimport { LightLeak } from \"@remotion/light-leaks\";\n\n<TransitionSeries>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneA />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Overlay durationInFrames={30}>\n    <LightLeak />\n  </TransitionSeries.Overlay>\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneB />\n  </TransitionSeries.Sequence>\n  <TransitionSeries.Transition\n    presentation={fade()}\n    timing={linearTiming({ durationInFrames: 15 })}\n  />\n  <TransitionSeries.Sequence durationInFrames={60}>\n    <SceneC />\n  </TransitionSeries.Sequence>\n</TransitionSeries>;\n```\n\n## Transition props\n\n`<TransitionSeries.Transition>` requires:\n\n- `presentation` — the visual effect (e.g. `fade()`, `slide()`, `wipe()`).\n- `timing` — controls speed and easing (e.g. `linearTiming()`, `springTiming()`).\n\n## Overlay props\n\n`<TransitionSeries.Overlay>` accepts:\n\n- `durationInFrames` — how long the overlay is visible (positive integer).\n- `offset?` — shifts the overlay relative to the cut point center. Positive = later, negative = earlier. Default: `0`.\n\n## Available transition types\n\nImport transitions from their respective modules:\n\n```tsx\nimport { fade } from \"@remotion/transitions/fade\";\nimport { slide } from \"@remotion/transitions/slide\";\nimport { wipe } from \"@remotion/transitions/wipe\";\nimport { flip } from \"@remotion/transitions/flip\";\nimport { clockWipe } from \"@remotion/transitions/clock-wipe\";\n```\n\n## Slide transition with direction\n\n```tsx\nimport { slide } from \"@remotion/transitions/slide\";\n\n<TransitionSeries.Transition\n  presentation={slide({ direction: \"from-left\" })}\n  timing={linearTiming({ durationInFrames: 20 })}\n/>;\n```\n\nDirections: `\"from-left\"`, `\"from-right\"`, `\"from-top\"`, `\"from-bottom\"`\n\n## Timing options\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\n// Linear timing - constant speed\nlinearTiming({ durationInFrames: 20 });\n\n// Spring timing - organic motion\nspringTiming({ config: { damping: 200 }, durationInFrames: 25 });\n```\n\n## Duration calculation\n\nTransitions overlap adjacent scenes, so the total composition length is **shorter** than the sum of all sequence durations. Overlays do **not** affect the total duration.\n\nFor example, with two 60-frame sequences and a 15-frame transition:\n\n- Without transitions: `60 + 60 = 120` frames\n- With transition: `60 + 60 - 15 = 105` frames\n\nAdding an overlay between two other sequences does not change the total.\n\n### Getting the duration of a transition\n\nUse the `getDurationInFrames()` method on the timing object:\n\n```tsx\nimport { linearTiming, springTiming } from \"@remotion/transitions\";\n\nconst linearDuration = linearTiming({\n  durationInFrames: 20,\n}).getDurationInFrames({ fps: 30 });\n// Returns 20\n\nconst springDuration = springTiming({\n  config: { damping: 200 },\n}).getDurationInFrames({ fps: 30 });\n// Returns calculated duration based on spring physics\n```\n\nFor `springTiming` without an explicit `durationInFrames`, the duration depends on `fps` because it calculates when the spring animation settles.\n\n### Calculating total composition duration\n\n```tsx\nimport { linearTiming } from \"@remotion/transitions\";\n\nconst scene1Duration = 60;\nconst scene2Duration = 60;\nconst scene3Duration = 60;\n\nconst timing1 = linearTiming({ durationInFrames: 15 });\nconst timing2 = linearTiming({ durationInFrames: 20 });\n\nconst transition1Duration = timing1.getDurationInFrames({ fps: 30 });\nconst transition2Duration = timing2.getDurationInFrames({ fps: 30 });\n\nconst totalDuration =\n  scene1Duration +\n  scene2Duration +\n  scene3Duration -\n  transition1Duration -\n  transition2Duration;\n// 60 + 60 + 60 - 15 - 20 = 145 frames\n```\n","frontmatter":{"name":"transitions","description":"Scene transitions and overlays for Remotion using TransitionSeries.","metadata":{"tags":"transitions, overlays, fade, slide, wipe, scenes"}},"isInternal":false,"tokens":1369,"sizeBytes":5816},{"name":"transparent-videos.md","path":"skills/remotion-best-practices/rules/transparent-videos.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/transparent-videos.md","title":"transparent-videos","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftransparent-videos.md -->","frontmatter":{"name":"transparent-videos","description":"Rendering transparent videos in Remotion","metadata":{"tags":"transparent, alpha, codec, vp9, prores, webm"}},"isInternal":false,"tokens":583,"sizeBytes":2267},{"name":"trimming.md","path":"skills/remotion-best-practices/rules/trimming.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/trimming.md","title":"trimming","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Ftrimming.md -->","frontmatter":{"name":"trimming","description":"Trimming patterns for Remotion - cut the beginning or end of animations","metadata":{"tags":"sequence, trim, clip, cut, offset"}},"isInternal":false,"tokens":297,"sizeBytes":1210},{"name":"videos.md","path":"skills/remotion-best-practices/rules/videos.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/rules/videos.md","title":"videos","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2Frules%2Fvideos.md -->","frontmatter":{"name":"videos","description":"Embedding videos in Remotion - trimming, volume, speed, looping, pitch","metadata":{"tags":"video, media, trim, volume, speed, loop, pitch"}},"isInternal":false,"tokens":974,"sizeBytes":3509},{"name":"SKILL.md","path":"skills/remotion-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion-best-practices/SKILL.md","title":"remotion-best-practices","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion-best-practices%2FSKILL.md -->","frontmatter":{"name":"remotion-best-practices","description":"Best practices for Remotion - Video creation in React","metadata":{"tags":"remotion, video, react, animation, composition"}},"isInternal":false,"tokens":868,"sizeBytes":3547},{"name":"SKILL.md","path":"skills/remotion/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/remotion/SKILL.md","title":"remotion","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fremotion%2FSKILL.md -->","frontmatter":{"name":"remotion","description":"Remotion renderer for json-render that turns JSON timeline specs into videos. Use when working with @json-render/remotion, building video compositions from JSON, creating video catalogs, or rendering AI-generated video timelines."},"isInternal":false,"tokens":721,"sizeBytes":3064},{"name":"SKILL.md","path":"skills/shadcn-svelte/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/shadcn-svelte/SKILL.md","title":"shadcn-svelte","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn-svelte%2FSKILL.md -->","frontmatter":{"name":"shadcn-svelte","description":"Pre-built shadcn-svelte components for json-render Svelte apps. Use when working with @json-render/shadcn-svelte, adding standard UI components to a Svelte catalog, or building Svelte web UIs with shadcn-svelte + Tailwind CSS components."},"isInternal":false,"tokens":1313,"sizeBytes":5497},{"name":"SKILL.md","path":"skills/shadcn/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/shadcn/SKILL.md","title":"shadcn","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fshadcn%2FSKILL.md -->","frontmatter":{"name":"shadcn","description":"Pre-built shadcn/ui components for json-render. Use when working with @json-render/shadcn, adding standard UI components to a catalog, or building web UIs with Radix UI + Tailwind CSS components."},"isInternal":false,"tokens":1536,"sizeBytes":6475},{"name":"output-patterns.md","path":"skills/skill-creator/references/output-patterns.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/skill-creator/references/output-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Foutput-patterns.md -->","isInternal":false,"tokens":355,"sizeBytes":1813},{"name":"workflows.md","path":"skills/skill-creator/references/workflows.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/skill-creator/references/workflows.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2Freferences%2Fworkflows.md -->","isInternal":false,"tokens":182,"sizeBytes":818},{"name":"SKILL.md","path":"skills/skill-creator/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/skill-creator/SKILL.md","title":"skill-creator","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fskill-creator%2FSKILL.md -->","frontmatter":{"name":"skill-creator","description":"Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.","license":"Complete terms in LICENSE.txt"},"isInternal":false,"tokens":3651,"sizeBytes":17837},{"name":"SKILL.md","path":"skills/solid/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/solid/SKILL.md","title":"solid","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsolid%2FSKILL.md -->","frontmatter":{"name":"solid","description":"SolidJS renderer for json-render. Use when building @json-render/solid catalogs/registries, wiring Renderer providers, implementing bindings/actions, or troubleshooting Solid-specific reactivity patterns."},"isInternal":false,"tokens":1242,"sizeBytes":5006},{"name":"SKILL.md","path":"skills/svelte/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/svelte/SKILL.md","title":"svelte","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fsvelte%2FSKILL.md -->","frontmatter":{"name":"svelte","description":"Svelte 5 renderer for json-render that turns JSON specs into Svelte component trees. Use when working with @json-render/svelte, building Svelte UIs from JSON, creating component catalogs, or rendering AI-generated specs."},"isInternal":false,"tokens":1807,"sizeBytes":7256},{"name":"SKILL.md","path":"skills/tanstack-start/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/tanstack-start/SKILL.md","title":"tanstack-start","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Ftanstack-start%2FSKILL.md -->","frontmatter":{"name":"tanstack-start","description":"Build JSON-defined TanStack Start applications with @json-render/tanstack-start. Use for Start route specs, splat routing, SSR loaders, head metadata, layouts, and client navigation. Do not use for generic TanStack Router apps that do not use json-render."},"isInternal":false,"tokens":1374,"sizeBytes":5912},{"name":"SKILL.md","path":"skills/vue/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/vue/SKILL.md","title":"vue","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fvue%2FSKILL.md -->","frontmatter":{"name":"vue","description":"Vue 3 renderer for json-render. Use when building Vue UIs from JSON specs, working with @json-render/vue, defining Vue component registries, or rendering AI-generated specs in Vue."},"isInternal":false,"tokens":1584,"sizeBytes":6399},{"name":"SKILL.md","path":"skills/xstate/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/xstate/SKILL.md","title":"xstate","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fxstate%2FSKILL.md -->","frontmatter":{"name":"xstate","description":"XState Store adapter for json-render's StateStore interface. Use when integrating json-render with @xstate/store for state management via @json-render/xstate."},"isInternal":false,"tokens":333,"sizeBytes":1310},{"name":"SKILL.md","path":"skills/yaml/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/yaml/SKILL.md","title":"yaml","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fyaml%2FSKILL.md -->","frontmatter":{"name":"yaml","description":"YAML wire format for json-render with streaming parser, prompt generation, and AI SDK transform. Use when working with @json-render/yaml, YAML-based spec streaming, yaml-spec/yaml-edit fences, or YAML prompt generation."},"isInternal":false,"tokens":1140,"sizeBytes":4609},{"name":"SKILL.md","path":"skills/zustand/SKILL.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/skills/zustand/SKILL.md","title":"zustand","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=skills%2Fzustand%2FSKILL.md -->","frontmatter":{"name":"zustand","description":"Zustand adapter for json-render's StateStore interface. Use when integrating json-render with Zustand for state management via @json-render/zustand."},"isInternal":false,"tokens":500,"sizeBytes":1908},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=.cursor%2Fmcp.json -->","frontmatter":{"mcpServers":{"json-render":{"command":"npx","args":["tsx","examples/mcp/server.ts","--stdio"]}}},"isInternal":false,"tokens":44,"sizeBytes":136},{"name":"README.md","path":"examples/devtools/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/examples/devtools/README.md","title":"devtools Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=examples%2Fdevtools%2FREADME.md -->","isInternal":false,"tokens":846,"sizeBytes":3447},{"name":"README.md","path":"packages/codegen/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/codegen/README.md","title":"codegen Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcodegen%2FREADME.md -->","isInternal":false,"tokens":661,"sizeBytes":2745},{"name":"README.md","path":"packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fcore%2FREADME.md -->","isInternal":false,"tokens":6526,"sizeBytes":27847},{"name":"README.md","path":"packages/devtools-react/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/devtools-react/README.md","title":"devtools-react Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-react%2FREADME.md -->","isInternal":false,"tokens":281,"sizeBytes":1079},{"name":"README.md","path":"packages/devtools-solid/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/devtools-solid/README.md","title":"devtools-solid Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-solid%2FREADME.md -->","isInternal":false,"tokens":217,"sizeBytes":828},{"name":"README.md","path":"packages/devtools-svelte/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/devtools-svelte/README.md","title":"devtools-svelte Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-svelte%2FREADME.md -->","isInternal":false,"tokens":212,"sizeBytes":785},{"name":"README.md","path":"packages/devtools-vue/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/devtools-vue/README.md","title":"devtools-vue Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools-vue%2FREADME.md -->","isInternal":false,"tokens":231,"sizeBytes":855},{"name":"README.md","path":"packages/devtools/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/devtools/README.md","title":"devtools Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdevtools%2FREADME.md -->","isInternal":false,"tokens":492,"sizeBytes":2103},{"name":"README.md","path":"packages/directives/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/directives/README.md","title":"directives Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fdirectives%2FREADME.md -->","isInternal":false,"tokens":920,"sizeBytes":3231},{"name":"README.md","path":"packages/eslint-config/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/eslint-config/README.md","title":"eslint-config Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Feslint-config%2FREADME.md -->","isInternal":false,"tokens":15,"sizeBytes":72},{"name":"README.md","path":"packages/image/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/image/README.md","title":"image Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fimage%2FREADME.md -->","isInternal":false,"tokens":1037,"sizeBytes":4154},{"name":"README.md","path":"packages/ink/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/ink/README.md","title":"ink Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fink%2FREADME.md -->","isInternal":false,"tokens":1101,"sizeBytes":4541},{"name":"README.md","path":"packages/jotai/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/jotai/README.md","title":"jotai Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fjotai%2FREADME.md -->","isInternal":false,"tokens":492,"sizeBytes":1875},{"name":"README.md","path":"packages/mcp/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/mcp/README.md","title":"mcp Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fmcp%2FREADME.md -->","isInternal":false,"tokens":910,"sizeBytes":3659},{"name":"README.md","path":"packages/next/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/next/README.md","title":"next Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fnext%2FREADME.md -->","isInternal":false,"tokens":801,"sizeBytes":3223},{"name":"README.md","path":"packages/react-email/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/react-email/README.md","title":"react-email Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-email%2FREADME.md -->","isInternal":false,"tokens":1039,"sizeBytes":4195},{"name":"README.md","path":"packages/react-native/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/react-native/README.md","title":"react-native Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-native%2FREADME.md -->","isInternal":false,"tokens":1756,"sizeBytes":7368},{"name":"README.md","path":"packages/react-pdf/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/react-pdf/README.md","title":"react-pdf Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-pdf%2FREADME.md -->","isInternal":false,"tokens":1138,"sizeBytes":4734},{"name":"README.md","path":"packages/react-three-fiber/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/react-three-fiber/README.md","title":"react-three-fiber Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact-three-fiber%2FREADME.md -->","isInternal":false,"tokens":470,"sizeBytes":1969},{"name":"README.md","path":"packages/react/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/react/README.md","title":"react Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Freact%2FREADME.md -->","isInternal":false,"tokens":3901,"sizeBytes":16933},{"name":"README.md","path":"packages/redux/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/redux/README.md","title":"redux Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fredux%2FREADME.md -->","isInternal":false,"tokens":472,"sizeBytes":1924},{"name":"README.md","path":"packages/remotion/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/remotion/README.md","title":"remotion Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fremotion%2FREADME.md -->","isInternal":false,"tokens":1721,"sizeBytes":7010},{"name":"README.md","path":"packages/shadcn-svelte/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/shadcn-svelte/README.md","title":"shadcn-svelte Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn-svelte%2FREADME.md -->","isInternal":false,"tokens":475,"sizeBytes":1866},{"name":"README.md","path":"packages/shadcn/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/shadcn/README.md","title":"shadcn Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fshadcn%2FREADME.md -->","isInternal":false,"tokens":1464,"sizeBytes":6171},{"name":"README.md","path":"packages/solid/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/solid/README.md","title":"solid Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsolid%2FREADME.md -->","isInternal":false,"tokens":1828,"sizeBytes":8847},{"name":"README.md","path":"packages/svelte/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/svelte/README.md","title":"svelte Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fsvelte%2FREADME.md -->","isInternal":false,"tokens":1155,"sizeBytes":4876},{"name":"README.md","path":"packages/tanstack-start/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/tanstack-start/README.md","title":"tanstack-start Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Ftanstack-start%2FREADME.md -->","isInternal":false,"tokens":1555,"sizeBytes":6712},{"name":"README.md","path":"packages/vue/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/vue/README.md","title":"vue Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fvue%2FREADME.md -->","isInternal":false,"tokens":3782,"sizeBytes":15282},{"name":"README.md","path":"packages/xstate/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/xstate/README.md","title":"xstate Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fxstate%2FREADME.md -->","isInternal":false,"tokens":303,"sizeBytes":1154},{"name":"README.md","path":"packages/yaml/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/yaml/README.md","title":"yaml Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fyaml%2FREADME.md -->","isInternal":false,"tokens":892,"sizeBytes":3628},{"name":"README.md","path":"packages/zustand/README.md","rawUrl":"https://raw.githubusercontent.com/vercel-labs/json-render/HEAD/packages/zustand/README.md","title":"zustand Documentation","category":"plugin-manifest","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/vercel-labs/json-render?path=packages%2Fzustand%2FREADME.md -->","isInternal":false,"tokens":500,"sizeBytes":1894}],"systemPromptSnippet":"<yakaai_skills repo=\"vercel-labs/json-render\">\n<!-- File: AGENTS.md (Tokens: ~1473 | Category: root-instruction) -->\n# AGENTS.md\n\nInstructions for AI coding agents working with this codebase.\n\n## Package Management\n\n**Always check the latest version before installing a package.**\n\nBefore adding or updating any dependency, verify the current latest version on npm:\n\n```bash\nnpm view <package-name> version\n```\n\nOr check multiple packages at once:\n\n```bash\nnpm view ai version\nnpm view @ai-sdk/provider-utils version\nnpm view zod version\n```\n\nThis ensures we don't install outdated versions that may have incompatible types or missing features.\n\n## Code Style\n\n- Do not use emojis in code or UI\n- Use shadcn CLI to add shadcn/ui components: `pnpm dlx shadcn@latest add <component>`\n- **Web app docs (`apps/web/`):** Never use Markdown table syntax (`| col | col |`). Always use HTML `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`. Markdown tables do not render correctly in the web app. Inside HTML table cells, curly braces must be escaped as JSX expressions (e.g. `<code>{'{ \"$state\": \"/path\" }'}</code>`) because MDX parses `{` as a JSX expression boundary.\n\n## AI SDK / AI Gateway\n\nWhen using the Vercel AI SDK (`ai` package) with AI Gateway, pass the model as a plain string identifier -- do not import a provider constructor:\n\n```ts\nimport { streamText } from \"ai\";\n\nconst result = streamText({\n  model: \"anthropic/claude-haiku-4.5\",\n  prompt: \"...\",\n});\n```\n\nThis requires `AI_GATEWAY_API_KEY` to be set in the environment. See `tests/e2e/` for examples.\n\n## Dev Servers\n\nAll apps and examples with dev servers use [portless](https://github.com/vercel-labs/portless) to avoid hardcoded ports. Portless assigns random ports and exposes each app via `.localhost` URLs.\n\nNaming convention:\n- Main web app: `json-render` → `json-render.localhost:1355`\n- Examples: `[name]-demo.json-render` → `[name]-demo.json-render.localhost:1355`\n\nWhen adding a new example that runs a dev server, wrap its `dev` script with `portless <name>`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"portless my-example-demo.json-render next dev --turbopack\"\n  }\n}\n```\n\nDo **not** add `--port` flags -- portless handles port assignment automatically. Do **not** add portless as a project dependency; it must be installed globally.\n\n## Workflow\n\n- Run `pnpm type-check` after each turn to ensure type safety\n- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync.\n- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content.\n- When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation:\n  - Package `README.md` files in `packages/*/README.md`\n  - Root `README.md` (if packages table, install commands, or examples are affected)\n  - Web app docs in `apps/web/` (if guides, API references, or examples need updating)\n  - Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)\n  - `AGENTS.md` (if workflow or conventions change)\n\n## Releasing\n\nReleases are manual, single-PR affairs. The maintainer controls the changelog voice and format.\n\nAll public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.\n\n### Preparing a release\n\nWhen asked to prepare a release (e.g. \"prepare v0.17.0\"):\n\n1. Create a branch (e.g. `prepare-v0.17.0`)\n2. Bump the version in `packages/core/package.json`\n3. Run `pnpm run version:sync` to update all other `@json-render/*` packages\n4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)\n5. **Fill documentation gaps** — every public package should have:\n   - A row in the root `README.md` packages table\n   - A renderer section in the root `README.md` (if it's a renderer)\n   - An API reference page at `apps/web/content/docs/api/<name>.mdx`\n   - An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`\n   - An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)\n   - A skill at `skills/<name>/SKILL.md`\n   - A `packages/<name>/README.md`\n6. **Run `pnpm type-check`** after all changes to verify nothing is broken\n7. Open a PR and merge to `main`\n\nCI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.\n\n### Scripts\n\n- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`\n- `pnpm run version:check` — verify all versions are in sync (runs in CI)\n- `pnpm run ci:publish` — build all packages and publish to npm (CI only)\n\n<!-- opensrc:start -->\n\n## Source Code Reference\n\nSource code for dependencies is available in `opensrc/` for deeper understanding of implementation details.\n\nSee `opensrc/sources.json` for the list of available packages and their versions.\n\nUse this source code when you need to understand how a package works internally, not just its types/interface.\n\n### Fetching Additional Source Code\n\nTo fetch source code for a package or repository you need to understand, run:\n\n```bash\nnpx opensrc <package>           # npm package (e.g., npx opensrc zod)\nnpx opensrc pypi:<package>      # Python package (e.g., npx opensrc pypi:requests)\nnpx opensrc crates:<package>    # Rust crate (e.g., npx opensrc crates:serde)\nnpx opensrc <owner>/<repo>      # GitHub repo (e.g., npx opensrc vercel/ai)\n```\n\n<!-- opensrc:end -->\n\n<!-- File: skills/codegen/SKILL.md (Tokens: ~615 | Category: anthropic-skill) -->\n---\nname: codegen\ndescription: Code generation utilities for json-render. Use when generating code from UI specs, building custom code exporters, traversing specs, or serializing props for @json-render/codegen.\n---\n\n# @json-render/codegen\n\nFramework-agnostic utilities for generating code from json-render UI trees. Use these to build custom code exporters for Next.js, Remix, or other frameworks.\n\n## Installation\n\n```bash\nnpm install @json-render/codegen\n```\n\n## Tree Traversal\n\n```typescript\nimport {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n} from \"@json-render/codegen\";\n\n// Walk the spec depth-first\ntraverseSpec(spec, (element, key, depth, parent) => {\n  console.log(`${\" \".repeat(depth * 2)}${key}: ${element.type}`);\n});\n\n// Get all component types used\nconst components = collectUsedComponents(spec);\n// Set { \"Card\", \"Metric\", \"Button\" }\n\n// Get all state paths referenced\nconst statePaths = collectStatePaths(spec);\n// Set { \"analytics/revenue\", \"user/name\" }\n\n// Get all action names\nconst actions = collectActions(spec);\n// Set { \"submit_form\", \"refresh_data\" }\n```\n\n## Serialization\n\n```typescript\nimport {\n  serializePropValue,\n  serializeProps,\n  escapeString,\n  type SerializeOptions,\n} from \"@json-render/codegen\";\n\n// Serialize a single value\nserializePropValue(\"hello\");\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue({ $state: \"/user/name\" });\n// { value: '{ $state: \"/user/name\" }', needsBraces: true }\n\n// Serialize props for JSX\nserializeProps({ title: \"Dashboard\", columns: 3, disabled: true });\n// 'title=\"Dashboard\" columns={3} disabled'\n\n// Escape strings for code\nescapeString('hello \"world\"');\n// 'hello \\\"world\\\"'\n```\n\n### SerializeOptions\n\n```typescript\ninterface SerializeOptions {\n  quotes?: \"single\" | \"double\";\n  indent?: number;\n}\n```\n\n## Types\n\n```typescript\nimport type { GeneratedFile, CodeGenerator } from \"@json-render/codegen\";\n\nconst myGenerator: CodeGenerator = {\n  generate(spec) {\n    return [\n      { path: \"package.json\", content: \"...\" },\n      { path: \"app/page.tsx\", content: \"...\" },\n    ];\n  },\n};\n```\n\n## Building a Custom Generator\n\n```typescript\nimport {\n  collectUsedComponents,\n  collectStatePaths,\n  traverseSpec,\n  serializeProps,\n  type GeneratedFile,\n} from \"@json-render/codegen\";\nimport type { Spec } from \"@json-render/core\";\n\nexport function generateNextJSProject(spec: Spec): GeneratedFile[] {\n  const files: GeneratedFile[] = [];\n  const components = collectUsedComponents(spec);\n  // Generate package.json, component files, main page...\n  return files;\n}\n```\n\n<!-- File: skills/core/SKILL.md (Tokens: ~3434 | Category: anthropic-skill) -->\n---\nname: core\ndescription: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.\n---\n\n# @json-render/core\n\nCore package for schema definition, catalog creation, and spec streaming.\n\n## Key Concepts\n\n- **Schema**: Defines the structure of specs and catalogs (use `defineSchema`)\n- **Catalog**: Maps component/action names to their definitions (use `defineCatalog`)\n- **Spec**: JSON output from AI that conforms to the schema\n- **SpecStream**: JSONL streaming format for progressive spec building\n\n## Experimental Decision-Model Composition\n\nFor decision-model composition, import `experimental_composeSpec` and `experimental_createEvaluator` from `@json-render/core`. These APIs are unreleased; use a source build until published, then pin exact versions. Experimental exports and `Experimental_` types can change in any release.\n\n- Run the Gateway evaluator server-side with `{ model: \"typesafe-ai/jev\", apiKey: process.env.AI_GATEWAY_API_KEY! }`. A plain model identifier is required; Jev is the current example; do not import a provider constructor.\n- Call `experimental_composeSpec({ catalog, candidates, prompt, evaluate, initialState, signal })`. It is an async generator; stream `step.spec` snapshots to your existing renderer and inspect `complete.stopReason` (`finish`, `limit`, `unavailable`). Errors and cancellation throw; retain the last snapshot as partial UI.\n- New trees default to `strategy: \"batch\"`: one evaluation selects root/membership, then a second arranges the selected elements when needed. The first snapshot contains selected content in catalog order under the root's default/first slot. Resource variants share one exclusive question; repeated counts include the root. Root selection takes precedence over conflicting speculative membership for that recipe/resource. Equal sibling positions retain catalog order. Combined layouts are validated before publication; cycles or excessive depth throw. `maxElements` caps batched creation (default 32). Limit-truncated selections or a missing required layout call return `limit`. Use `strategy: \"sequential\"` for legacy `next`/`parent` adapters or sequential creation. Edits stay sequential.\n- Batched trace steps use `choice: \"select\" | \"layout\"` and an `answers` record. Count each trace as one evaluation, including its tokens and latency once. Custom evaluators must answer every offered question; names/choices are opaque and include `root`/`select_*`, then `parent_*`/`order_*` for batches.\n- For follow-up edits, pass the selected version as `initialSpec`. It is cloned and validated; the evaluator may add, replace, remove non-root subtrees, or move/reorder them. Unchanged IDs, bindings, and state are preserved. Optional `elementDescriptions` shares identifying descriptions without exposing raw props/state. `initialState` overrides the seed state. Seeds must be valid trees within the catalog, expression subset, and depth limit. Matching recipes consume usage/resource limits; removals/replacements release them. Replacements/moves use two evaluations (select target, then recipe/destination), each counted against the budget. Treat operation and position keys as opaque.\n- Supply atomic candidates with `{ id, description, element: { type, props, on?, visible? }, root?, maxUses?, resource? }`. Catalog alone is insufficient: the app must supply values and binding recipes. Jev chooses elements and parent slots, never free-form text or code. It never executes actions.\n- Candidates are configured component instances, not page templates. Build them from current app records/operations or bind props to `initialState`; offer explicit alternatives for chart types, field configurations, and layout variants. The model chooses grouping and order within those options. Name required sections in prompts; structural validity does not imply semantic completeness.\n- V1 supports flat Spec catalogs, named slots, literals, `$state`, `$bindState`, and state visibility. No prebuilt children, repeat/watch, computed/template/conditional props, or custom directives. Success/error callbacks must reference allowed actions. Events must be declared in the component catalog.\n- Props and action params are validated against initial state without applying schema transforms/defaults. Supply valid initial values and validate/authorize action calls at runtime. Built-ins without parameter schemas get name validation only.\n- `root` defaults true, `maxUses` defaults one, shared `resource` values make alternatives mutually exclusive. Defaults: 32 evaluations (terminal calls included; no extra finish call for batches), depth eight, 10-second Gateway timeout per call. Supply an overall abort signal.\n- Candidate descriptions, prompt, instructions, topology, and explicit `context` are sent to the evaluator. Initial state and raw props/binding values are not sent automatically.\n- For custom providers implement `Experimental_CompositionEvaluator`: accept `{ state, questions, signal }`, return `{ answers: { [question]: { choice, confidence? } }, usage?: { inputTokens? } }`. Only return offered criteria keys.\n\nSee `packages/core/README.md` and `/docs/jev` for app integration and source-build instructions. The web playground is an example consumer, not a dependency of the API.\n\n## Defining a Schema\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nexport const schema = defineSchema((s) => ({\n  spec: s.object({\n    // Define spec structure\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate, // Optional custom AI prompt\n});\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"./schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button component\",\n    },\n  },\n});\n```\n\n## Generating AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt(); // Uses schema's promptTemplate\nconst systemPrompt = catalog.prompt({ customRules: [\"Rule 1\", \"Rule 2\"] });\n```\n\n## SpecStream Utilities\n\nFor streaming AI responses (JSONL patches):\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process streaming chunks\nconst { result, newPatches } = compiler.push(chunk);\n\n// Get final result\nconst finalSpec = compiler.getResult();\n```\n\n## Form Values in Action Handlers\n\nUse `findFormValue(\"email\", params, state)` to read a direct parameter, a dotted parameter key (such as `\"form.email\"`), a matching flat state key, or a slash-delimited path (such as `\"/form/email\"`) in nested state. Parameter values are literal, so emails and URLs containing dots are preserved. For action bindings that read nested state, use `{ $state: \"/form/email\" }`; the resolver passes that value to the handler. A bare `\"email\"` field name does not search nested `state.form.email`.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads a value from the state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - evaluates a visibility condition and picks a branch\n- **`{ \"$template\": \"Hello, ${/user/name}!\" }`** - interpolates `${/path}` references with state values\n- **`{ \"$computed\": \"fnName\", \"args\": { \"key\": <expression> } }`** - calls a registered function with resolved args\n\n`$cond` uses the same syntax as visibility conditions (`$state`, `eq`, `neq`, `not`, arrays for AND). `$then` and `$else` can themselves be expressions (recursive).\n\nComponents do not use a `statePath` prop for two-way binding. Instead, use `{ \"$bindState\": \"/path\" }` on the natural value prop (e.g. `value`, `checked`, `pressed`).\n\n```json\n{\n  \"color\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"#007AFF\",\n    \"$else\": \"#8E8E93\"\n  },\n  \"label\": { \"$template\": \"Welcome, ${/user/name}!\" },\n  \"fullName\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\n```typescript\nimport { resolvePropValue, resolveElementProps } from \"@json-render/core\";\n\nconst resolved = resolveElementProps(element.props, { stateModel: myState });\n```\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": { \"value\": { \"$bindState\": \"/form/country\" }, \"options\": [\"US\", \"Canada\"] },\n  \"watch\": {\n    \"/form/country\": { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } }\n  },\n  \"children\": []\n}\n```\n\nWatchers only fire on value changes, not on initial render.\n\n## Validation\n\nBuilt-in validation functions: `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`.\n\nCross-field validation uses `$state` expressions in args:\n\n```typescript\nimport { check } from \"@json-render/core\";\n\ncheck.required(\"Field is required\");\ncheck.matches(\"/form/password\", \"Passwords must match\");\ncheck.lessThan(\"/form/endDate\", \"Must be before end date\");\ncheck.greaterThan(\"/form/startDate\", \"Must be after start date\");\ncheck.requiredIf(\"/form/enableNotifications\", \"Required when enabled\");\n```\n\n## User Prompt Builder\n\nBuild structured user prompts with optional spec refinement and state context:\n\n```typescript\nimport { buildUserPrompt } from \"@json-render/core\";\n\n// Fresh generation\nbuildUserPrompt({ prompt: \"create a todo app\" });\n\n// Refinement with edit modes (default: patch-only)\nbuildUserPrompt({ prompt: \"add a toggle\", currentSpec: spec, editModes: [\"patch\", \"merge\"] });\n\n// With runtime state\nbuildUserPrompt({ prompt: \"show data\", state: { todos: [] } });\n```\n\nAvailable edit modes: `\"patch\"` (RFC 6902 JSON Patch), `\"merge\"` (RFC 7396 Merge Patch), `\"diff\"` (unified diff).\n\n## Spec Validation\n\nValidate spec structure and auto-fix common issues:\n\n```typescript\nimport { validateSpec, autoFixSpec } from \"@json-render/core\";\n\nconst { valid, issues } = validateSpec(spec);\n// issues include: missing_child, invalid_visible (malformed conditions),\n// repeat_without_children, repeat_item_outside_scope, repeat_state_mismatch\n\nconst { spec: fixed, fixDetails } = autoFixSpec(spec);\n// fixDetails entries are { message, lossy }. Lossless fixes relocate\n// misplaced fields; lossy fixes prune dangling children references.\n// In a repair loop, withhold lossy fixes until retries are exhausted:\nconst attempt = autoFixSpec(spec, { lossy: retriesExhausted });\n```\n\n## Visibility Conditions\n\nControl element visibility with state-based conditions. `VisibilityContext` is `{ stateModel: StateModel }`.\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\n// Syntax\n{ \"$state\": \"/path\" }                    // truthiness\n{ \"$state\": \"/path\", \"not\": true }      // falsy\n{ \"$state\": \"/path\", \"eq\": value }      // equality\n[ cond1, cond2 ]                         // implicit AND\n\n// Helpers\nvisibility.when(\"/path\")                 // { $state: \"/path\" }\nvisibility.unless(\"/path\")               // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)              // { $state: \"/path\", eq: val }\nvisibility.and(cond1, cond2)             // { $and: [cond1, cond2] }\nvisibility.or(cond1, cond2)              // { $or: [cond1, cond2] }\nvisibility.always                        // true\nvisibility.never                         // false\n```\n\n## Built-in Actions in Schema\n\nSchemas can declare `builtInActions` -- actions that are always available at runtime and auto-injected into prompts:\n\n```typescript\nconst schema = defineSchema(builder, {\n  builtInActions: [\n    { name: \"setState\", description: \"Update a value in the state model\" },\n  ],\n});\n```\n\nThese appear in prompts as `[built-in]` and don't require handlers in `defineRegistry`.\n\n## StateStore\n\nThe `StateStore` interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The `createStateStore` factory creates a simple in-memory implementation:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/core\";\n\nconst store = createStateStore({ count: 0 });\n\nstore.get(\"/count\");         // 0\nstore.set(\"/count\", 1);      // updates and notifies subscribers\nstore.update({ \"/a\": 1, \"/b\": 2 }); // batch update\n\nstore.subscribe(() => {\n  console.log(store.getSnapshot()); // { count: 1 }\n});\n```\n\nThe `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`, `getSnapshot()`, `subscribe(listener)`.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineSchema` | Create a new schema |\n| `defineCatalog` | Create a catalog from schema |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `resolvePropValue` | Resolve a single prop expression against data |\n| `resolveElementProps` | Resolve all prop expressions in an element |\n| `buildUserPrompt` | Build user prompts with refinement and state context |\n| `buildEditUserPrompt` | Build user prompt for editing existing specs |\n| `buildEditInstructions` | Generate prompt section for available edit modes |\n| `isNonEmptySpec` | Check if spec has root and at least one element |\n| `deepMergeSpec` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |\n| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |\n| `EditMode` | Type: `\"patch\" \\| \"merge\" \\| \"diff\"` |\n| `validateSpec` | Validate spec structure |\n| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |\n| `createSpecStreamCompiler` | Stream JSONL patches into spec |\n| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |\n| `parseSpecStreamLine` | Parse single JSONL line |\n| `applySpecStreamPatch` | Apply patch to object |\n| `StateStore` | Interface for plugging in external state management |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `check` | TypeScript helpers for creating validation checks |\n| `BuiltInAction` | Type for built-in action definitions (`name` + `description`) |\n| `ActionBinding` | Action binding type (includes `preventDefault` field) |\n\n<!-- File: skills/devtools-react/SKILL.md (Tokens: ~380 | Category: anthropic-skill) -->\n---\nname: devtools-react\ndescription: React adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-react in a React json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-react\n\nReact adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-react\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing React json-render provider so it can observe the renderer's state, actions, and streams:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut.\n\n## Imperative controls\n\nUse `useJsonRenderDevtools` from any mounted React descendant:\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nThe hook returns `null` in production or before the component has mounted. Keep server-side stream instrumentation in `@json-render/devtools`.\n\n<!-- File: skills/devtools-solid/SKILL.md (Tokens: ~274 | Category: anthropic-skill) -->\n---\nname: devtools-solid\ndescription: SolidJS adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-solid in a SolidJS json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-solid\n\nSolidJS adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing SolidJS json-render provider:\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n\n<!-- File: skills/devtools-svelte/SKILL.md (Tokens: ~279 | Category: anthropic-skill) -->\n---\nname: devtools-svelte\ndescription: Svelte 5 adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-svelte in a Svelte json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-svelte\n\nSvelte 5 adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-svelte\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Svelte json-render provider:\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n\n<!-- File: skills/devtools-vue/SKILL.md (Tokens: ~290 | Category: anthropic-skill) -->\n---\nname: devtools-vue\ndescription: Vue adapter for the json-render devtools inspector. Use whenever the user wants to add, configure, or troubleshoot @json-render/devtools-vue in a Vue json-render app, including spec inspection, state editing, action and stream timelines, catalog browsing, or DOM picking.\n---\n\n# @json-render/devtools-vue\n\nVue adapter for the json-render devtools panel. Read `skills/devtools/SKILL.md` for the shared panel capabilities, props, docking behavior, and server-side stream taps.\n\n## Install\n\n```bash\nnpm install @json-render/devtools @json-render/devtools-vue\n```\n\n## Mount the panel\n\nMount `JsonRenderDevtools` inside the existing Vue json-render provider:\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\nThe component renders nothing in production builds. The panel provides the shared devtools controls, including the `Ctrl`/`Cmd` + `Shift` + `J` shortcut. Keep server-side stream instrumentation in `@json-render/devtools`.\n\n<!-- File: skills/devtools/SKILL.md (Tokens: ~2194 | Category: anthropic-skill) -->\n---\nname: devtools\ndescription: Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include \"add devtools\", \"debug json-render\", \"inspect the spec\", \"why is this element not rendering\", \"see the state at runtime\", or requests to tap streams / capture action logs for `@json-render/devtools`.\n---\n\n# @json-render/devtools\n\nA floating inspector panel for json-render apps. Framework-agnostic core + per-framework adapters (React, Vue, Svelte, Solid).\n\nProduction-safe: the component renders no devtools UI when `process.env.NODE_ENV === \"production\"`, including when a bundler replaces the expression in a browser without a `process` global. Unbundled environments without `process.env` retain development behavior.\n\n## Install\n\nInstall the core package plus the adapter that matches the host app's renderer.\n\n```bash\n# React\nnpm install @json-render/devtools @json-render/devtools-react\n\n# Vue\nnpm install @json-render/devtools @json-render/devtools-vue\n\n# Svelte\nnpm install @json-render/devtools @json-render/devtools-svelte\n\n# Solid\nnpm install @json-render/devtools @json-render/devtools-solid\n```\n\n## Drop-in usage\n\nPlace `<JsonRenderDevtools />` anywhere inside the existing `<JSONUIProvider>` (or framework equivalent). No other wiring required.\n\n### React\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-react\";\n\n<JSONUIProvider registry={registry} handlers={handlers}>\n  <Renderer spec={spec} registry={registry} />\n  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />\n</JSONUIProvider>;\n```\n\n### Vue\n\n```vue\n<script setup>\nimport { JsonRenderDevtools } from \"@json-render/devtools-vue\";\n</script>\n\n<template>\n  <JSONUIProvider :registry=\"registry\">\n    <Renderer :spec=\"spec\" :registry=\"registry\" />\n    <JsonRenderDevtools :spec=\"spec\" :catalog=\"catalog\" :messages=\"messages\" />\n  </JSONUIProvider>\n</template>\n```\n\n### Svelte\n\n```svelte\n<script>\n  import { JsonRenderDevtools } from \"@json-render/devtools-svelte\";\n</script>\n\n<JSONUIProvider {registry}>\n  <Renderer {spec} {registry} />\n  <JsonRenderDevtools {spec} {catalog} {messages} />\n</JSONUIProvider>\n```\n\n### Solid\n\n```tsx\nimport { JsonRenderDevtools } from \"@json-render/devtools-solid\";\n\n<JSONUIProvider registry={registry}>\n  <Renderer spec={spec()} registry={registry} />\n  <JsonRenderDevtools\n    spec={spec()}\n    catalog={catalog}\n    messages={messages()}\n  />\n</JSONUIProvider>;\n```\n\n## Controls\n\n- Floating toggle appears bottom-right.\n- Hotkey: `Ctrl`/`Cmd` + `Shift` + `J` (configurable via `hotkey` prop).\n- Drawer is resizable; height persists to localStorage.\n\n## Props\n\n- `spec` (`Spec | null`) — current spec.\n- `catalog` (`Catalog | null`) — catalog definition; required for the Catalog panel.\n- `messages` (`UIMessage[]`) — AI SDK `useChat` messages; scanned for spec data parts.\n- `initialOpen` (`boolean`) — start open.\n- `position` (`\"bottom-right\" | \"bottom-left\" | \"right\"`) — dock + toggle corner. `\"bottom-*\"` docks at the bottom; `\"right\"` docks at the right edge full-height (recommended for app-shells that already use `100vh` or fixed bottom bars).\n- `hotkey` (`string | false`) — `\"mod+shift+j\"` by default.\n- `bufferSize` (`number`) — event ring-buffer cap, default 500.\n- `reserveSpace` (`boolean`, default `true`) — when true the panel pushes the host app by applying `padding-bottom` / `padding-right` on `body`. Set to `false` to keep the panel as a pure overlay.\n- `allowDockToggle` (`boolean`, default `true`) — show a toolbar button so the user can flip the panel between bottom-dock and right-dock. User choice persists to `localStorage` and overrides `position` on subsequent mounts. Pass `false` to lock the dock to `position`.\n- `onEvent` (`(DevtoolsEvent) => void`) — optional tap.\n\n## Panels\n\n- **Spec** — element tree rooted at `spec.root`; props/visibility/events/watchers detail; integrated `validateSpec` warnings.\n- **State** — every JSON Pointer path with inline edit via `store.set`.\n- **Actions** — dispatched actions timeline (name, params, result/error, duration).\n- **Stream** — spec patches, text chunks, token usage, lifecycle markers grouped by generation.\n- **Catalog** — components + actions declared in the catalog with prop chips.\n\n## Picker (toolbar)\n\nThe element picker is a toolbar button in the panel header (Chrome-DevTools-style), not a tab. Click it to activate pick mode, then click any rendered element in the page — selection jumps to the Spec tab with that element focused. `Esc` cancels.\n\n## Reserved space & docking\n\nThe panel can dock at the bottom or the right edge, and by default the user can flip between the two with a toolbar button (the choice persists to `localStorage`). Set `allowDockToggle={false}` if the host app only works with one dock — the button is hidden and the dock is locked to `position`.\n\nPick an initial dock that fits your layout:\n\n- **Bottom dock (default)** — works best for docs / marketing / content-flow sites and for app shells built with a `height: 100%` chain (`html { height: 100% }` → `body { height: 100% }` → `.app { height: 100% }`). The panel writes its height to `--jr-devtools-offset-bottom` and applies matching `padding-bottom` to `body`, so non-fixed content naturally makes room.\n- **Right dock** (`position=\"right\"`) — recommended for app-shell layouts that use `100vh` or `position: fixed; bottom: 0`. Right docking sidesteps the bottom edge entirely and writes its width to `--jr-devtools-offset-right` instead.\n\nApps that use `100vh`, `position: fixed`, or `position: sticky` can opt specific elements in with the published CSS custom properties:\n\n```css\n.composer   { bottom: var(--jr-devtools-offset-bottom, 0); }\n.sidebar    { right:  var(--jr-devtools-offset-right,  0); }\n.app-shell  { height: calc(100vh - var(--jr-devtools-offset-bottom, 0)); }\n```\n\nIf the automatic body padding causes problems with a particular layout, pass `reserveSpace={false}` to make the panel a pure overlay — the CSS custom properties are still published so you can reserve space manually.\n\n(`--jr-devtools-offset` is kept as a back-compat alias for whichever edge is currently active.)\n\n## Multiple renderers on one page (e.g. a chat)\n\nA single `<JsonRenderDevtools />` can inspect many `<Renderer />` instances at once — a chat where each assistant message renders its own spec, a dashboard made of several independent widgets, etc. The recipe:\n\n1. **One top-level `<JSONUIProvider>`** so every renderer shares one state store and one action dispatcher. Devtools lives inside this provider and sees everything through it.\n2. **Per-renderer specs, shared state** — each assistant message renders `<Renderer spec={msgSpec} registry={registry} />` directly, not wrapped in its own `StateProvider`. State paths from different messages must not collide.\n3. **Namespace state per turn** — when the source is an AI stream, hand the agent a unique `messageId` and require every element key (`<id>-root`) and state path (`/<id>/count`) to be prefixed with it.\n4. **Pass `spec={latest}` + `messages={all}`** — `spec` drives the Spec panel (usually the newest assistant message's spec), while `messages` feeds the Stream panel with patches from every turn.\n5. **Actions and the picker are already global** — `registerActionObserver` captures dispatches from any `ActionProvider` in the tree, and `data-jr-key` is written by the renderer itself, so Pick works across every rendered element regardless of which message produced it.\n\nSee `examples/devtools` for a full AI chat wired this way.\n\n## Imperative API (React only)\n\n```tsx\nimport { useJsonRenderDevtools } from \"@json-render/devtools-react\";\n\nconst devtools = useJsonRenderDevtools();\ndevtools?.open();\ndevtools?.toggle();\ndevtools?.recordEvent({ kind: \"stream-text\", at: Date.now(), text: \"hi\" });\n```\n\nReturns `null` in production or before the component mounts.\n\n## Server-side stream tap\n\nCapture spec patches at the API route so events persist server-side or flow into your own telemetry.\n\n```ts\nimport { tapJsonRenderStream, createEventStore } from \"@json-render/devtools\";\nimport { pipeJsonRender } from \"@json-render/core\";\n\nconst events = createEventStore({ bufferSize: 1000 });\nconst tapped = tapJsonRenderStream(result.toUIMessageStream(), events);\nwriter.merge(pipeJsonRender(tapped));\n```\n\nYAML equivalent: `tapYamlStream`.\n\n## Under the hood\n\n- **Shadow-DOM isolated panel** — the panel's styles never leak into the host app and vice versa.\n- **Ring-buffered event store** — capped log of devtools events (state changes, action dispatches, stream patches, etc.).\n- **Action observer registry** — each framework's `ActionProvider` reports via `notifyActionDispatch` / `notifyActionSettle` in `@json-render/core`; devtools subscribes via `registerActionObserver`.\n- **Picker element tagging** — while devtools is mounted, `ElementRenderer` wraps each rendered element in `<span data-jr-key=\"...\" style=\"display:contents\">` so the picker can map DOM → spec key. No layout impact.\n\n<!-- File: skills/directives/SKILL.md (Tokens: ~1761 | Category: anthropic-skill) -->\n---\nname: directives\ndescription: Pre-built custom directives for json-render — formatting, math, string manipulation, and i18n. Use when working with @json-render/directives, defining custom directives with defineDirective, or adding $format, $math, $concat, $count, $truncate, $pluralize, $join, or $t to specs.\n---\n\n# @json-render/directives\n\nPre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.\n\n## Quick Start\n\n```typescript\nimport { standardDirectives } from '@json-render/directives';\n\n// Wire into prompt generation\nconst prompt = catalog.prompt({ directives: standardDirectives });\n\n// Wire into the renderer (React example)\nimport { JSONUIProvider, Renderer } from '@json-render/react';\n\n<JSONUIProvider registry={registry} directives={standardDirectives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\nTo add factory directives like `createI18nDirective`, spread the array:\n\n```typescript\nimport { standardDirectives, createI18nDirective } from '@json-render/directives';\n\nconst directives = [...standardDirectives, createI18nDirective(config)];\n```\n\n## Defining Custom Directives\n\nUse `defineDirective` from `@json-render/core`:\n\n```typescript\nimport { defineDirective, resolvePropValue } from '@json-render/core';\nimport { z } from 'zod';\n\nconst doubleDirective = defineDirective({\n  name: '$double',\n  description: 'Double a numeric value.',\n  schema: z.object({\n    $double: z.unknown(),\n  }),\n  resolve(value, ctx) {\n    const resolved = resolvePropValue(value.$double, ctx);\n    return (resolved as number) * 2;\n  },\n});\n```\n\nRules:\n- Name must start with `$`\n- Name must not conflict with built-in keys (`$state`, `$cond`, `$computed`, `$template`, `$item`, `$index`, `$bindState`, `$bindItem`)\n- Resolvers should call `resolvePropValue` on sub-values to support composition\n\n## Built-in Directives\n\n### `$format` — Locale-aware value formatting\n\nFormats values using `Intl` formatters. Supports `date`, `currency`, `number`, and `percent`.\n\n```json\n{ \"$format\": \"currency\", \"value\": { \"$state\": \"/cart/total\" }, \"currency\": \"USD\" }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/user/createdAt\" } }\n{ \"$format\": \"number\", \"value\": 1234567, \"notation\": \"compact\" }\n{ \"$format\": \"percent\", \"value\": 0.75 }\n{ \"$format\": \"date\", \"value\": { \"$state\": \"/post/createdAt\" }, \"style\": \"relative\" }\n```\n\nFields: `$format` (date | currency | number | percent), `value` (any expression), `locale?` (string), `currency?` (string, default \"USD\"), `notation?` (string), `style?` (\"relative\" for relative dates), `options?` (extra Intl options).\n\n### `$math` — Arithmetic operations\n\n```json\n{ \"$math\": \"add\", \"a\": { \"$state\": \"/subtotal\" }, \"b\": { \"$state\": \"/tax\" } }\n{ \"$math\": \"round\", \"a\": 3.7 }\n```\n\nOperations: `add`, `subtract`, `multiply`, `divide`, `mod`, `min`, `max`, `round`, `floor`, `ceil`, `abs`. Unary ops (`round`, `floor`, `ceil`, `abs`) only use `a`. Division by zero returns `0`.\n\nFields: `$math` (operation enum), `a?` (first operand, default 0), `b?` (second operand, default 0).\n\n### `$concat` — String concatenation\n\n```json\n{ \"$concat\": [{ \"$state\": \"/user/firstName\" }, \" \", { \"$state\": \"/user/lastName\" }] }\n```\n\nFields: `$concat` (array of values to resolve and join into a string).\n\n### `$count` — Array/string length\n\n```json\n{ \"$count\": { \"$state\": \"/cart/items\" } }\n```\n\nReturns `.length` of arrays or strings, `0` for other types.\n\nFields: `$count` (value to count).\n\n### `$truncate` — Text truncation\n\n```json\n{ \"$truncate\": { \"$state\": \"/post/body\" }, \"length\": 140, \"suffix\": \"...\" }\n```\n\nFields: `$truncate` (value to truncate), `length?` (max chars, default 100), `suffix?` (string, default \"...\").\n\n### `$pluralize` — Singular/plural forms\n\n```json\n{ \"$pluralize\": { \"$state\": \"/cart/itemCount\" }, \"one\": \"item\", \"other\": \"items\", \"zero\": \"no items\" }\n```\n\nOutput: `\"3 items\"`, `\"1 item\"`, or `\"no items\"`.\n\nFields: `$pluralize` (count value), `one` (singular label), `other` (plural label), `zero?` (zero label).\n\n### `$join` — Join array elements\n\n```json\n{ \"$join\": { \"$state\": \"/tags\" }, \"separator\": \", \" }\n```\n\nFields: `$join` (array to join), `separator?` (string, default \", \").\n\n### `createI18nDirective` — Internationalization factory\n\n```typescript\nimport { createI18nDirective } from '@json-render/directives';\n\nconst tDirective = createI18nDirective({\n  locale: 'en',\n  messages: {\n    en: { \"greeting\": \"Hello, {{name}}!\", \"checkout.submit\": \"Place Order\" },\n    es: { \"greeting\": \"Hola, {{name}}!\", \"checkout.submit\": \"Realizar Pedido\" },\n  },\n  fallbackLocale: 'en',\n});\n```\n\nUsage in specs:\n\n```json\n{ \"$t\": \"checkout.submit\" }\n{ \"$t\": \"greeting\", \"params\": { \"name\": { \"$state\": \"/user/name\" } } }\n```\n\nFields: `$t` (translation key), `params?` (interpolation parameters, values accept expressions).\n\nConfig: `locale` (current locale), `messages` (Record<locale, Record<key, string>>), `fallbackLocale?` (fallback when key missing).\n\n## Composition\n\nDirectives compose naturally — each resolver calls `resolvePropValue` on its inputs, so directives can wrap other directives or built-in expressions:\n\n```json\n{\n  \"$format\": \"currency\",\n  \"value\": { \"$math\": \"multiply\", \"a\": { \"$state\": \"/price\" }, \"b\": { \"$state\": \"/qty\" } },\n  \"currency\": \"USD\"\n}\n```\n\nResolves inside-out: `$state` reads from state, `$math` multiplies, `$format` formats as currency.\n\n## Wiring into Renderers\n\nAll four renderers (React, Vue, Svelte, Solid) accept `directives` on their provider and `createRenderer` output:\n\n```tsx\n// Provider pattern\n<JSONUIProvider registry={registry} directives={directives}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n\n// createRenderer pattern\nconst MyRenderer = createRenderer(catalog, components);\n<MyRenderer spec={spec} directives={directives} />\n```\n\nFor prompt generation, pass the same array:\n\n```typescript\nconst prompt = catalog.prompt({ directives });\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `formatDirective` | `$format` directive definition |\n| `mathDirective` | `$math` directive definition |\n| `concatDirective` | `$concat` directive definition |\n| `countDirective` | `$count` directive definition |\n| `truncateDirective` | `$truncate` directive definition |\n| `pluralizeDirective` | `$pluralize` directive definition |\n| `joinDirective` | `$join` directive definition |\n| `createI18nDirective` | Factory for `$t` i18n directive |\n| `standardDirectives` | Array of all 7 non-factory directives |\n| `I18nConfig` | Type for i18n configuration |\n\n<!-- File: skills/image/SKILL.md (Tokens: ~686 | Category: anthropic-skill) -->\n---\nname: image\ndescription: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.\n---\n\n# @json-render/image\n\nImage renderer that converts JSON specs into SVG and PNG images using Satori.\n\n## Quick Start\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"frame\",\n  elements: {\n    frame: {\n      type: \"Frame\",\n      props: { width: 1200, height: 630, backgroundColor: \"#1a1a2e\" },\n      children: [\"heading\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Hello World\", level: \"h1\", color: \"#ffffff\" },\n      children: [],\n    },\n  },\n};\n\nconst png = await renderToPng(spec, {\n  fonts: [{ name: \"Inter\", data: fontData, weight: 400, style: \"normal\" }],\n});\n```\n\n## Using Standard Components\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, standardComponentDefinitions } from \"@json-render/image\";\n\nexport const imageCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Adding Custom Components\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n});\n```\n\n## Standard Components\n\n| Component | Category | Description |\n|-----------|----------|-------------|\n| `Frame` | Root | Root container. Defines width, height, background. Must be root. |\n| `Box` | Layout | Container with padding, margin, border, absolute positioning |\n| `Row` | Layout | Horizontal flex layout |\n| `Column` | Layout | Vertical flex layout |\n| `Heading` | Content | h1-h4 heading text |\n| `Text` | Content | Body text with full styling |\n| `Image` | Content | Image from URL |\n| `Divider` | Decorative | Horizontal line separator |\n| `Spacer` | Decorative | Empty vertical space |\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `renderToSvg` | Render spec to SVG string |\n| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) |\n| `schema` | Image element schema |\n| `standardComponents` | Pre-built component registry |\n| `standardComponentDefinitions` | Catalog definitions for AI prompts |\n\n## Sub-path Exports\n\n| Export | Description |\n|--------|-------------|\n| `@json-render/image` | Full package: schema, components, render functions |\n| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) |\n| `@json-render/image/catalog` | Standard component definitions and types |\n| `@json-render/image/render` | Render functions only |\n\n<!-- File: skills/ink/SKILL.md (Tokens: ~2443 | Category: anthropic-skill) -->\n---\nname: ink\ndescription: Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.\n---\n\n# @json-render/ink\n\nInk terminal renderer that converts JSON specs into interactive terminal component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/ink/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/ink/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/ink\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    CustomWidget: {\n      props: z.object({ title: z.string() }),\n      slots: [],\n      description: \"Custom widget\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    CustomWidget: ({ props }) => <Text>{props.title}</Text>,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <JSONUIProvider initialState={{}}>\n      <Renderer spec={spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Spec Structure (Flat Element Map)\n\nThe Ink schema uses a flat element map with a root key:\n\n```json\n{\n  \"root\": \"main\",\n  \"elements\": {\n    \"main\": {\n      \"type\": \"Box\",\n      \"props\": { \"flexDirection\": \"column\", \"padding\": 1 },\n      \"children\": [\"heading\", \"content\"]\n    },\n    \"heading\": {\n      \"type\": \"Heading\",\n      \"props\": { \"text\": \"Dashboard\", \"level\": \"h1\" },\n      \"children\": []\n    },\n    \"content\": {\n      \"type\": \"Text\",\n      \"props\": { \"text\": \"Hello from the terminal!\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Standard Components\n\n### Layout\n- `Box` - Flexbox layout container (like a terminal `<div>`). Use for grouping, spacing, borders, alignment. Default flexDirection is row.\n- `Text` - Text output with optional styling (color, bold, italic, etc.)\n- `Newline` - Inserts blank lines. Must be inside a Box with flexDirection column.\n- `Spacer` - Flexible empty space that expands along the main axis.\n\n### Content\n- `Heading` - Section heading (h1: bold+underlined, h2: bold, h3: bold+dimmed, h4: dimmed)\n- `Divider` - Horizontal separator with optional centered title\n- `Badge` - Colored inline label (variants: default, info, success, warning, error)\n- `Spinner` - Animated loading spinner with optional label\n- `ProgressBar` - Horizontal progress bar (0-1)\n- `Sparkline` - Inline chart using Unicode block characters\n- `BarChart` - Horizontal bar chart with labels and values\n- `Table` - Tabular data with headers and rows\n- `List` - Bulleted or numbered list\n- `ListItem` - Structured list row with title, subtitle, leading/trailing text\n- `Card` - Bordered container with optional title\n- `KeyValue` - Key-value pair display\n- `Link` - Clickable URL with optional label\n- `StatusLine` - Status message with colored icon (info, success, warning, error)\n- `Markdown` - Renders markdown text with terminal styling\n\n### Interactive\n- `TextInput` - Text input field (events: submit, change)\n- `Select` - Selection menu with arrow key navigation (events: change)\n- `MultiSelect` - Multi-selection with space to toggle (events: change, submit)\n- `ConfirmInput` - Yes/No confirmation prompt (events: confirm, deny)\n- `Tabs` - Tab bar navigation with left/right arrow keys (events: change)\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop of form components\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Event System\n\nComponents use `emit` to fire named events. The element's `on` field maps events to action bindings:\n\n```tsx\nCustomButton: ({ props, emit }) => (\n  <Box>\n    <Text>{props.label}</Text>\n    {/* emit(\"press\") triggers the action bound in the spec's on.press */}\n  </Box>\n),\n```\n\n```json\n{\n  \"type\": \"CustomButton\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\n`setState`, `pushState`, and `removeState` are built-in and handled automatically:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n```\n\n## Repeat (Dynamic Lists)\n\nUse the `repeat` field on a container element to render items from a state array:\n\n```json\n{\n  \"type\": \"Box\",\n  \"props\": { \"flexDirection\": \"column\" },\n  \"repeat\": { \"statePath\": \"/items\", \"key\": \"id\" },\n  \"children\": [\"item-row\"]\n}\n```\n\nInside repeated children, use `{ \"$item\": \"field\" }` to read from the current item and `{ \"$index\": true }` for the current index.\n\nFor nested lists, an inner repeat can use `{ \"statePath\": { \"$item\": \"children\" } }` to iterate an array on the enclosing item.\n\n## Streaming\n\nUse `useUIStream` to progressively render specs from JSONL patch streams:\n\n```tsx\nimport { useUIStream } from \"@json-render/ink\";\n\nconst { spec, send, isStreaming } = useUIStream({ api: \"/api/generate\" });\n```\n\n## Server-Side Prompt Generation\n\nUse the `./server` export to generate AI system prompts from your catalog:\n\n```typescript\nimport { catalog } from \"./catalog\";\n\nconst systemPrompt = catalog.prompt({ system: \"You are a terminal assistant.\" });\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched via the event system |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n| `FocusProvider` | Manage focus across interactive components |\n| `JSONUIProvider` | Combined provider for all contexts |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/ink\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## createRenderer (Higher-Level API)\n\n```tsx\nimport { createRenderer } from \"@json-render/ink\";\nimport { standardComponents } from \"@json-render/ink\";\nimport { catalog } from \"./catalog\";\n\nconst InkRenderer = createRenderer(catalog, {\n  ...standardComponents,\n  // custom component overrides here\n});\n\n// InkRenderer includes all providers (state, visibility, actions, focus)\nrender(\n  <InkRenderer spec={spec} state={{ activeTab: \"overview\" }} />\n);\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `createRenderer` | Higher-level: creates a component with built-in providers |\n| `JSONUIProvider` | Combined provider for all contexts |\n| `schema` | Ink flat element map schema (includes built-in state actions) |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way binding for `$bindState`/`$bindItem` expressions |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useOptionalValidation` | Non-throwing variant of useValidation |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n| `Components` | Typed component map (catalog-aware) |\n| `Actions` | Typed action map (catalog-aware) |\n| `ComponentContext` | Typed component context (catalog-aware) |\n| `flatToTree` | Convert flat element map to tree structure |\n\n## Terminal UI Design Guidelines\n\n- Use Box for layout (flexDirection, padding, gap). Default flexDirection is row.\n- Terminal width is ~80-120 columns. Prefer vertical layouts (flexDirection: column) for main structure.\n- Use borderStyle on Box for visual grouping (single, double, round, bold).\n- Use named terminal colors: red, green, yellow, blue, magenta, cyan, white, gray.\n- Use Heading for section titles, Divider to separate sections, Badge for status, KeyValue for labeled data, Card for bordered groups.\n- Use Tabs for multi-view UIs with visible conditions on child content.\n- Use Sparkline for inline trends and BarChart for comparing values.\n\n<!-- File: skills/jotai/SKILL.md (Tokens: ~528 | Category: anthropic-skill) -->\n---\nname: jotai\ndescription: Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.\n---\n\n# @json-render/jotai\n\nJotai adapter for json-render's `StateStore` interface. Wire a Jotai atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/jotai @json-render/core @json-render/react jotai\n```\n\n## Usage\n\n```tsx\nimport { atom } from \"jotai\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom that holds the json-render state\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = jotaiStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Jotai */}\n</StateProvider>\n```\n\n### With a Shared Jotai Store\n\nWhen your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:\n\n```tsx\nimport { atom, createStore } from \"jotai\";\nimport { Provider as JotaiProvider } from \"jotai/react\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst jStore = createStore();\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\n\nconst store = jotaiStateStore({ atom: uiAtom, store: jStore });\n\n<JotaiProvider store={jStore}>\n  <StateProvider store={store}>\n    {/* Both json-render and useAtom() see the same state */}\n  </StateProvider>\n</JotaiProvider>\n```\n\n## API\n\n### `jotaiStateStore(options)`\n\nCreates a `StateStore` backed by a Jotai atom.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `WritableAtom<StateModel, [StateModel], void>` | Yes | A writable atom holding the state model |\n| `store` | Jotai `Store` | No | The Jotai store instance. Defaults to a new store. Pass your own to share state with `<Provider>`. |\n\n<!-- File: skills/mcp/SKILL.md (Tokens: ~898 | Category: anthropic-skill) -->\n---\nname: mcp\ndescription: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.\n---\n\n# @json-render/mcp\n\nMCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Quick Start\n\n### Server (Node.js)\n\n```typescript\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...shadcnComponentDefinitions },\n  actions: {},\n});\n\nconst server = createMcpApp({\n  name: \"My App\",\n  version: \"1.0.0\",\n  catalog,\n  html: fs.readFileSync(\"dist/index.html\", \"utf-8\"),\n});\n\nawait server.connect(new StdioServerTransport());\n```\n\n### Client (React, inside iframe)\n\n```tsx\nimport { useJsonRenderApp } from \"@json-render/mcp/app\";\nimport { JSONUIProvider, Renderer } from \"@json-render/react\";\n\nfunction McpAppView({ registry }) {\n  const { spec, loading, error } = useJsonRenderApp();\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting...</div>;\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Architecture\n\n1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource\n2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs\n3. The HTML resource is a Vite-bundled single-file React app with json-render renderers\n4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs\n\n## Server API\n\n- `createMcpApp(options)` - main entry, creates a full MCP server\n- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server\n- `registerJsonRenderResource(server, options)` - register the UI resource\n\n## Client API (`@json-render/mcp/app`)\n\n- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`\n- `buildAppHtml(options)` - generate HTML from bundled JS/CSS\n\n## Building the iframe HTML\n\nBundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:\n\n```typescript\n// vite.config.ts\nimport { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\n\nexport default defineConfig({\n  plugins: [react(), viteSingleFile()],\n  build: { outDir: \"dist\" },\n});\n```\n\n## Client Configuration\n\n### Cursor (`.cursor/mcp.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## Dependencies\n\n```bash\n# Server\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n\n# Client (iframe)\nnpm install @json-render/react @json-render/shadcn react react-dom\n\n# Build tools\nnpm install -D vite @vitejs/plugin-react vite-plugin-singlefile\n```\n\n<!-- File: skills/next/SKILL.md (Tokens: ~1385 | Category: anthropic-skill) -->\n---\nname: next\ndescription: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.\n---\n\n# @json-render/next\n\nNext.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.\n\n## Quick Start\n\n```bash\nnpm install @json-render/core @json-render/react @json-render/next\n```\n\n### 1. Define Your Spec\n\n```typescript\n// lib/spec.ts\nimport type { NextAppSpec } from \"@json-render/next\";\n\nexport const spec: NextAppSpec = {\n  metadata: {\n    title: { default: \"My App\", template: \"%s | My App\" },\n    description: \"A json-render Next.js application\",\n  },\n  layouts: {\n    main: {\n      root: \"shell\",\n      elements: {\n        shell: { type: \"Container\", props: {}, children: [\"nav\", \"slot\"] },\n        nav: { type: \"NavBar\", props: { links: [\n          { href: \"/\", label: \"Home\" },\n          { href: \"/about\", label: \"About\" },\n        ]}, children: [] },\n        slot: { type: \"Slot\", props: {}, children: [] },\n      },\n    },\n  },\n  routes: {\n    \"/\": {\n      layout: \"main\",\n      metadata: { title: \"Home\" },\n      page: {\n        root: \"hero\",\n        elements: {\n          hero: { type: \"Card\", props: { title: \"Welcome\" }, children: [] },\n        },\n      },\n    },\n    \"/about\": {\n      layout: \"main\",\n      metadata: { title: \"About\" },\n      page: {\n        root: \"content\",\n        elements: {\n          content: { type: \"Card\", props: { title: \"About Us\" }, children: [] },\n        },\n      },\n    },\n  },\n};\n```\n\n### 2. Create the App\n\n```typescript\n// lib/app.ts\nimport { createNextApp } from \"@json-render/next/server\";\nimport { spec } from \"./spec\";\n\nexport const { Page, generateMetadata, generateStaticParams } = createNextApp({\n  spec,\n  loaders: {\n    // Server-side data loaders (optional)\n    loadPost: async ({ slug }) => {\n      const post = await getPost(slug as string);\n      return { post };\n    },\n  },\n});\n```\n\n### 3. Wire Up Route Files\n\n```tsx\n// app/[[...slug]]/page.tsx\nexport { Page as default, generateMetadata, generateStaticParams } from \"@/lib/app\";\n```\n\n```tsx\n// app/[[...slug]]/layout.tsx\nimport { NextAppProvider } from \"@json-render/next\";\nimport { registry, handlers } from \"@/lib/registry\";\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>\n        <NextAppProvider registry={registry} handlers={handlers}>\n          {children}\n        </NextAppProvider>\n      </body>\n    </html>\n  );\n}\n```\n\n## Key Concepts\n\n### NextAppSpec\n\nThe top-level spec defines an entire Next.js application:\n\n- **metadata**: Root-level SEO metadata (title template, description, OpenGraph)\n- **layouts**: Reusable layout element trees (each must include a `Slot` component)\n- **routes**: Route definitions keyed by URL pattern\n- **state**: Global initial state shared across all routes\n\n### Route Patterns\n\nRoutes use Next.js URL conventions:\n\n- `\"/\"` -- home page\n- `\"/about\"` -- static route\n- `\"/blog/[slug]\"` -- dynamic segment\n- `\"/docs/[...path]\"` -- catch-all segment\n- `\"/settings/[[...path]]\"` -- optional catch-all segment\n\n### Layouts\n\nLayouts wrap page content. Every layout MUST include a `Slot` component where page content will be rendered. Layouts are defined once in `spec.layouts` and referenced by routes via the `layout` field.\n\n### Built-in Components\n\n- **Slot**: Placeholder in layouts where page content is rendered\n- **Link**: Client-side navigation link (wraps `next/link`)\n\n### Built-in Actions\n\n- **setState**: Update state value. Params: `{ statePath, value }`\n- **pushState**: Append to array. Params: `{ statePath, value, clearStatePath? }`\n- **removeState**: Remove from array by index. Params: `{ statePath, index }`\n- **navigate**: Client-side navigation. Params: `{ href }`\n\n### Data Loaders\n\nServer-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.\n\n```typescript\ncreateNextApp({\n  spec,\n  loaders: {\n    loadPost: async ({ slug }) => {\n      const post = await db.post.findUnique({ where: { slug } });\n      return { post };\n    },\n  },\n});\n```\n\n### SSR\n\nPages are server-rendered automatically. The `createNextApp` `Page` component is an async Server Component that:\n\n1. Matches the route from the spec\n2. Runs server-side data loaders\n3. Generates metadata\n4. Passes the resolved spec to the client renderer for hydration\n\n### Entry Points\n\n- `@json-render/next` -- Client components (NextAppProvider, PageRenderer, Link)\n- `@json-render/next/server` -- Server utilities (createNextApp, matchRoute, schema)\n\n## API Reference\n\n### Server Exports (`@json-render/next/server`)\n\n- `createNextApp(options)` -- Create Page, generateMetadata, generateStaticParams\n- `schema` -- Custom schema for Next.js apps (for AI catalog generation)\n- `matchRoute(spec, pathname)` -- Match a URL to a route spec\n- `resolveMetadata(spec, route)` -- Resolve metadata for a route\n- `slugToPath(slug)` -- Convert catch-all slug array to pathname\n- `collectStaticParams(spec)` -- Collect static params for all routes\n\n### Client Exports (`@json-render/next`)\n\n- `NextAppProvider` -- Context provider for registry and handlers\n- `PageRenderer` -- Renders a page spec with optional layout\n- `NextErrorBoundary` -- Error boundary component\n- `NextLoading` -- Loading state component\n- `NextNotFound` -- Not-found component\n- `Link` -- Built-in navigation component (wraps next/link)\n\n<!-- File: skills/react-email/SKILL.md (Tokens: ~1525 | Category: anthropic-skill) -->\n---\nname: react-email\ndescription: React Email renderer for json-render that turns JSON specs into HTML or plain-text emails using @react-email/components and @react-email/render. Use when working with @json-render/react-email, building transactional or marketing emails from JSON, creating email catalogs, rendering AI-generated email specs, or when the user mentions react-email, HTML email, or transactional email.\nmetadata:\n  tags: react-email, email, json-render, html email, transactional email\n---\n\n# @json-render/react-email\n\nReact Email renderer that converts JSON specs into HTML or plain-text email output.\n\n## Quick Start\n\n```typescript\nimport { renderToHtml } from \"@json-render/react-email\";\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email\";\nimport { defineCatalog } from \"@json-render/core\";\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n\nconst spec = {\n  root: \"html-1\",\n  elements: {\n    \"html-1\": { type: \"Html\", props: { lang: \"en\", dir: \"ltr\" }, children: [\"head-1\", \"body-1\"] },\n    \"head-1\": { type: \"Head\", props: {}, children: [] },\n    \"body-1\": {\n      type: \"Body\",\n      props: { style: { backgroundColor: \"#f6f9fc\" } },\n      children: [\"container-1\"],\n    },\n    \"container-1\": {\n      type: \"Container\",\n      props: { style: { maxWidth: \"600px\", margin: \"0 auto\", padding: \"20px\" } },\n      children: [\"heading-1\", \"text-1\"],\n    },\n    \"heading-1\": { type: \"Heading\", props: { text: \"Welcome\" }, children: [] },\n    \"text-1\": { type: \"Text\", props: { text: \"Thanks for signing up.\" }, children: [] },\n  },\n};\n\nconst html = await renderToHtml(spec);\n```\n\n## Spec Structure (Element Tree)\n\nSame flat element tree as `@json-render/react`: `root` key plus `elements` map. Root must be `Html`; children of `Html` should be `Head` and `Body`. Use `Container` (e.g. max-width 600px) inside `Body` for client-safe layout.\n\n## Creating a Catalog and Registry\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToHtml } from \"@json-render/react-email\";\nimport { standardComponentDefinitions } from \"@json-render/react-email/catalog\";\nimport { Container, Heading, Text } from \"@react-email/components\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Alert: {\n      props: z.object({\n        message: z.string(),\n        variant: z.enum([\"info\", \"success\", \"warning\"]).nullable(),\n      }),\n      slots: [],\n      description: \"A highlighted message block\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Alert: ({ props }) => (\n      <Container style={{ padding: 16, backgroundColor: \"#eff6ff\", borderRadius: 8 }}>\n        <Text style={{ margin: 0 }}>{props.message}</Text>\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\n## Server-Side Render APIs\n\n| Function | Purpose |\n|----------|---------|\n| `renderToHtml(spec, options?)` | Render spec to HTML email string |\n| `renderToPlainText(spec, options?)` | Render spec to plain-text email string |\n\n`RenderOptions`: `registry`, `includeStandard` (default true), `state` (for `$state` / `$cond`).\n\n## Visibility and State\n\nSupports `visible` conditions, `$state`, `$cond`, repeat (`repeat.statePath`), nested repeat paths with `{ \"$item\": \"field\" }`, and the same expression syntax as `@json-render/react`. Use `state` in `RenderOptions` when rendering server-side so expressions resolve.\n\n## Server-Safe Import\n\nImport schema and catalog without React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email/server\";\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create type-safe component registry from catalog |\n| `Renderer` | Render spec in browser (e.g. preview); use with `JSONUIProvider` for state/actions |\n| `createRenderer` | Standalone renderer component with state/actions/validation |\n| `renderToHtml` | Server: spec to HTML string |\n| `renderToPlainText` | Server: spec to plain-text string |\n| `schema` | Email element schema |\n| `standardComponents` | Pre-built component implementations |\n| `standardComponentDefinitions` | Catalog definitions (Zod props) |\n\n## Sub-path Exports\n\n| Path | Purpose |\n|------|---------|\n| `@json-render/react-email` | Full package |\n| `@json-render/react-email/server` | Schema and catalog only (no React) |\n| `@json-render/react-email/catalog` | Standard component definitions and types |\n| `@json-render/react-email/render` | Render functions only |\n\n## Standard Components\n\nAll components accept a `style` prop (object) for inline styles. Use inline styles for email client compatibility; avoid external CSS.\n\n### Document structure\n\n| Component | Description |\n|-----------|-------------|\n| `Html` | Root wrapper (lang, dir). Children: Head, Body. |\n| `Head` | Email head section. |\n| `Body` | Body wrapper; use `style` for background. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Constrain width (e.g. max-width 600px). |\n| `Section` | Group content; table-based for compatibility. |\n| `Row` | Horizontal row. |\n| `Column` | Column in a Row; set width via style. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (as: h1–h6). |\n| `Text` | Body text. |\n| `Link` | Hyperlink (text, href). |\n| `Button` | CTA link styled as button (text, href). |\n| `Image` | Image from URL (src, alt, width, height). |\n| `Hr` | Horizontal rule. |\n\n### Utility\n\n| Component | Description |\n|-----------|-------------|\n| `Preview` | Inbox preview text (inside Html). |\n| `Markdown` | Markdown content as email-safe HTML. |\n\n## Email Best Practices\n\n- Keep width constrained (e.g. Container max-width 600px).\n- Use inline styles or React Email's style props; many clients strip `<style>` blocks.\n- Prefer table-based layout (Section, Row, Column) for broad client support.\n- Use absolute URLs for images; many clients block relative or cid: references in some contexts.\n- Test in multiple clients (Gmail, Outlook, Apple Mail); use a preview tool or Litmus-like service when possible.\n\n<!-- File: skills/react-native/SKILL.md (Tokens: ~1557 | Category: anthropic-skill) -->\n---\nname: react-native\ndescription: React Native renderer for json-render that turns JSON specs into native mobile UIs. Use when working with @json-render/react-native, building React Native UIs from JSON, creating mobile component catalogs, or rendering AI-generated specs on mobile.\n---\n\n# @json-render/react-native\n\nReact Native renderer that converts JSON specs into native mobile component trees with standard components, data binding, visibility, actions, and dynamic props.\n\n## Quick Start\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-native/schema\";\nimport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n} from \"@json-render/react-native/catalog\";\nimport { defineRegistry, Renderer, type Components } from \"@json-render/react-native\";\nimport { z } from \"zod\";\n\n// Create catalog with standard + custom components\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Icon: {\n      props: z.object({ name: z.string(), size: z.number().nullable(), color: z.string().nullable() }),\n      slots: [],\n      description: \"Icon display\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n\n// Register only custom components (standard ones are built-in)\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => <Ionicons name={props.name} size={props.size ?? 24} />,\n  } as Components<typeof catalog>,\n});\n\n// Render\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{}}>\n          <Renderer spec={spec} registry={registry} />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Standard Components\n\n### Layout\n- `Container` - wrapper with padding, background, border radius\n- `Row` - horizontal flex layout with gap, alignment\n- `Column` - vertical flex layout with gap, alignment\n- `ScrollContainer` - scrollable area (vertical or horizontal)\n- `SafeArea` - safe area insets for notch/home indicator\n- `Pressable` - touchable wrapper that triggers actions on press\n- `Spacer` - fixed or flexible spacing\n- `Divider` - thin line separator\n\n### Content\n- `Heading` - heading text (levels 1-6)\n- `Paragraph` - body text\n- `Label` - small label text\n- `Image` - image display with sizing modes\n- `Avatar` - circular avatar image\n- `Badge` - small status badge\n- `Chip` - tag/chip for categories\n\n### Input\n- `Button` - pressable button with variants\n- `TextInput` - text input field\n- `Switch` - toggle switch\n- `Checkbox` - checkbox with label\n- `Slider` - range slider\n- `SearchBar` - search input\n\n### Feedback\n- `Spinner` - loading indicator\n- `ProgressBar` - progress indicator\n\n### Composite\n- `Card` - card container with optional header\n- `ListItem` - list row with title, subtitle, accessory\n- `Modal` - bottom sheet modal\n\n## Visibility Conditions\n\nUse `visible` on elements. Syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `[ cond1, cond2 ]` for AND.\n\n## Pressable + setState Pattern\n\nUse `Pressable` with the built-in `setState` action for interactive UIs like tab bars:\n\n```json\n{\n  \"type\": \"Pressable\",\n  \"props\": {\n    \"action\": \"setState\",\n    \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n  },\n  \"children\": [\"home-icon\", \"home-label\"]\n}\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved at render time:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- Nested lists use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" } } }` inside an enclosing repeat.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n\n```json\n{\n  \"type\": \"TextInput\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\n## Built-in Actions\n\nThe `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions:\n\n```json\n{ \"action\": \"setState\", \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider` | Handle actions dispatched from components |\n| `VisibilityProvider` | Enable conditional rendering based on state |\n| `ValidationProvider` | Form field validation |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>\n\nstore.set(\"/count\", 1); // React re-renders automatically\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `Renderer` | Render a spec using a registry |\n| `schema` | React Native element tree schema |\n| `standardComponentDefinitions` | Catalog definitions for all standard components |\n| `standardActionDefinitions` | Catalog definitions for standard actions |\n| `standardComponents` | Pre-built component implementations |\n| `createStandardActionHandlers` | Create handlers for standard actions |\n| `useStateStore` | Access state context |\n| `useStateValue` | Get single value from state |\n| `useBoundProp` | Two-way state binding via `$bindState`/`$bindItem` |\n| `useStateBinding` | _(deprecated)_ Legacy two-way binding by path |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management |\n\n<!-- File: skills/react-pdf/SKILL.md (Tokens: ~931 | Category: anthropic-skill) -->\n---\nname: react-pdf\ndescription: React PDF renderer for json-render. Use when generating PDF documents from JSON specs, working with @json-render/react-pdf, or rendering specs to PDF buffers/streams/files.\n---\n\n# @json-render/react-pdf\n\nReact PDF renderer that generates PDF documents from JSON specs using `@react-pdf/renderer`.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\n## Quick Start\n\n```typescript\nimport { renderToBuffer } from \"@json-render/react-pdf\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: Spec = {\n  root: \"doc\",\n  elements: {\n    doc: { type: \"Document\", props: { title: \"Invoice\" }, children: [\"page\"] },\n    page: {\n      type: \"Page\",\n      props: { size: \"A4\" },\n      children: [\"heading\", \"table\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoice #1234\", level: \"h1\" },\n      children: [],\n    },\n    table: {\n      type: \"Table\",\n      props: {\n        columns: [\n          { header: \"Item\", width: \"60%\" },\n          { header: \"Price\", width: \"40%\", align: \"right\" },\n        ],\n        rows: [\n          [\"Widget A\", \"$10.00\"],\n          [\"Widget B\", \"$25.00\"],\n        ],\n      },\n      children: [],\n    },\n  },\n};\n\nconst buffer = await renderToBuffer(spec);\n```\n\n## Render APIs\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from \"@json-render/react-pdf\";\n\n// In-memory buffer\nconst buffer = await renderToBuffer(spec);\n\n// Readable stream (pipe to HTTP response)\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\n// Direct to file\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nAll render functions accept an optional second argument: `{ registry?, state?, handlers? }`.\n\n## Standard Components\n\n| Component | Description |\n|-----------|-------------|\n| `Document` | Top-level PDF wrapper (must be root) |\n| `Page` | Page with size (A4, LETTER), orientation, margins |\n| `View` | Generic container (padding, margin, background, border) |\n| `Row`, `Column` | Flex layout with gap, align, justify |\n| `Heading` | h1-h4 heading text |\n| `Text` | Body text (fontSize, color, weight, alignment) |\n| `Image` | Image from URL or base64 |\n| `Link` | Hyperlink with text and href |\n| `Table` | Data table with typed columns and rows |\n| `List` | Ordered or unordered list |\n| `Divider` | Horizontal line separator |\n| `Spacer` | Empty vertical space |\n| `PageNumber` | Current page number and total pages |\n\n## Custom Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry, renderToBuffer } from \"@json-render/react-pdf\";\nimport { standardComponentDefinitions } from \"@json-render/react-pdf/catalog\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({ label: z.string(), color: z.string().nullable() }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Badge: ({ props }) => (\n      <View style={{ backgroundColor: props.color ?? \"#e5e7eb\", padding: 4 }}>\n        <Text>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## External Store (Controlled Mode)\n\nPass a `StateStore` for full control over state:\n\nNested lists can set `repeat.statePath` to `{ \"$item\": \"field\" }` inside an enclosing repeat.\n\n```typescript\nimport { createStateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\nstore.set(\"/invoice/total\", 200);\n```\n\n## Server-Safe Import\n\nImport schema and catalog without pulling in React:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-pdf/server\";\n```\n\n<!-- File: skills/react-three-fiber/SKILL.md (Tokens: ~1389 | Category: anthropic-skill) -->\n---\nname: react-three-fiber\ndescription: React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs.\n---\n\n# @json-render/react-three-fiber\n\nReact Three Fiber renderer for json-render. 19 built-in 3D components.\n\n## Two Entry Points\n\n| Entry Point | Exports | Use For |\n|-------------|---------|---------|\n| `@json-render/react-three-fiber/catalog` | `threeComponentDefinitions` | Catalog schemas (no R3F dependency, safe for server) |\n| `@json-render/react-three-fiber` | `threeComponents`, `ThreeRenderer`, `ThreeCanvas`, schemas | R3F implementations and renderer |\n\n## Usage Pattern\n\nPick the 3D components you need from the standard definitions:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { threeComponentDefinitions } from \"@json-render/react-three-fiber/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { threeComponents, ThreeCanvas } from \"@json-render/react-three-fiber\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Box: threeComponentDefinitions.Box,\n    Sphere: threeComponentDefinitions.Sphere,\n    AmbientLight: threeComponentDefinitions.AmbientLight,\n    DirectionalLight: threeComponentDefinitions.DirectionalLight,\n    OrbitControls: threeComponentDefinitions.OrbitControls,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Box: threeComponents.Box,\n    Sphere: threeComponents.Sphere,\n    AmbientLight: threeComponents.AmbientLight,\n    DirectionalLight: threeComponents.DirectionalLight,\n    OrbitControls: threeComponents.OrbitControls,\n  },\n});\n```\n\n## Rendering\n\n### ThreeCanvas (convenience wrapper)\n\n```tsx\n<ThreeCanvas\n  spec={spec}\n  registry={registry}\n  shadows\n  camera={{ position: [5, 5, 5], fov: 50 }}\n  style={{ width: \"100%\", height: \"100vh\" }}\n/>\n```\n\n### Manual Canvas setup\n\n```tsx\nimport { Canvas } from \"@react-three/fiber\";\nimport { ThreeRenderer } from \"@json-render/react-three-fiber\";\n\n<Canvas shadows>\n  <ThreeRenderer spec={spec} registry={registry}>\n    {/* Additional R3F elements */}\n  </ThreeRenderer>\n</Canvas>\n```\n\n## Available Components (19)\n\n### Primitives (7)\n- `Box` -- width, height, depth, material\n- `Sphere` -- radius, widthSegments, heightSegments, material\n- `Cylinder` -- radiusTop, radiusBottom, height, material\n- `Cone` -- radius, height, material\n- `Torus` -- radius, tube, material\n- `Plane` -- width, height, material\n- `Capsule` -- radius, length, material\n\nAll primitives share: `position`, `rotation`, `scale`, `castShadow`, `receiveShadow`, `material`.\n\n### Lights (4)\n- `AmbientLight` -- color, intensity\n- `DirectionalLight` -- position, color, intensity, castShadow\n- `PointLight` -- position, color, intensity, distance, decay\n- `SpotLight` -- position, color, intensity, angle, penumbra\n\n### Other (8)\n- `Group` -- container with position/rotation/scale, supports children\n- `Model` -- GLTF/GLB loader via url prop\n- `Environment` -- HDRI environment map (preset, background, blur, intensity)\n- `Fog` -- linear fog (color, near, far)\n- `GridHelper` -- reference grid (size, divisions, color)\n- `Text3D` -- SDF text (text, fontSize, color, anchorX, anchorY)\n- `PerspectiveCamera` -- camera (position, fov, near, far, makeDefault)\n- `OrbitControls` -- orbit controls (enableDamping, enableZoom, autoRotate)\n\n## Shared Schemas\n\nReusable Zod schemas for custom 3D catalog definitions:\n\n```typescript\nimport { vector3Schema, materialSchema, transformProps, shadowProps } from \"@json-render/react-three-fiber\";\nimport { z } from \"zod\";\n\n// Custom 3D component\nconst myComponentDef = {\n  props: z.object({\n    ...transformProps,\n    ...shadowProps,\n    material: materialSchema.nullable(),\n    myCustomProp: z.string(),\n  }),\n  description: \"My custom 3D component\",\n};\n```\n\n## Material Schema\n\n```typescript\nmaterialSchema = z.object({\n  color: z.string().nullable(),         // default \"#ffffff\"\n  metalness: z.number().nullable(),     // default 0\n  roughness: z.number().nullable(),     // default 1\n  emissive: z.string().nullable(),      // default \"#000000\"\n  emissiveIntensity: z.number().nullable(), // default 1\n  opacity: z.number().nullable(),       // default 1\n  transparent: z.boolean().nullable(),  // default false\n  wireframe: z.boolean().nullable(),    // default false\n});\n```\n\n## Spec Format\n\n3D specs use the standard json-render flat element format:\n\n```json\n{\n  \"root\": \"scene\",\n  \"elements\": {\n    \"scene\": {\n      \"type\": \"Group\",\n      \"props\": { \"position\": [0, 0, 0] },\n      \"children\": [\"light\", \"box\"]\n    },\n    \"light\": {\n      \"type\": \"AmbientLight\",\n      \"props\": { \"intensity\": 0.5 },\n      \"children\": []\n    },\n    \"box\": {\n      \"type\": \"Box\",\n      \"props\": {\n        \"position\": [0, 0.5, 0],\n        \"material\": { \"color\": \"#4488ff\", \"metalness\": 0.3, \"roughness\": 0.7 }\n      },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Dependencies\n\nPeer dependencies required:\n- `@react-three/fiber` >= 8.0.0\n- `@react-three/drei` >= 9.0.0\n- `three` >= 0.160.0\n- `react` ^19.0.0\n- `zod` ^4.0.0\n\n<!-- File: skills/react/SKILL.md (Tokens: ~2653 | Category: anthropic-skill) -->\n---\nname: react\ndescription: React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.\n---\n\n# @json-render/react\n\nReact renderer that converts JSON specs into React component trees.\n\n## Quick Start\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => <div>{props.title}{children}</div>,\n  },\n});\n\nfunction App({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { z } from \"zod\";\n\n// Create catalog with props schemas\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"Clickable button\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n      description: \"Card container with title\",\n    },\n    Layout: {\n      props: z.object({}),\n      slots: [\"default\", \"header\", \"footer\"],\n      description: \"Layout with named content regions\",\n    },\n  },\n});\n\n// Define component implementations with type-safe props\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Button: ({ props }) => (\n      <button className={props.variant}>{props.label}</button>\n    ),\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h2>{props.title}</h2>\n        {children}\n      </div>\n    ),\n    Layout: ({ children, slots }) => (\n      <div>\n        <header>{slots?.header}</header>\n        <main>{children}</main>\n        <footer>{slots?.footer}</footer>\n      </div>\n    ),\n  },\n});\n```\n\n## Spec Structure (Element Tree)\n\nThe React schema uses an element tree format:\n\n```json\n{\n  \"root\": {\n    \"type\": \"Card\",\n    \"props\": { \"title\": \"Hello\" },\n    \"children\": [{ \"type\": \"Button\", \"props\": { \"label\": \"Click me\" } }]\n  }\n}\n```\n\n## Named Slots\n\nUse `children` for the `\"default\"` slot. Use the element's top-level `slots` object for other slot names declared by the catalog:\n\n```json\n{\n  \"type\": \"Layout\",\n  \"props\": {},\n  \"children\": [\"main\"],\n  \"slots\": {\n    \"header\": [\"heading\"],\n    \"footer\": [\"actions\"]\n  }\n}\n```\n\nRegistry components receive named content as `slots?.header`, `slots?.footer`, and so on. Do not use `slots.default`.\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state. New syntax: `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, `{ \"$and\": [cond1, cond2] }` for AND, `{ \"$or\": [cond1, cond2] }` for OR. Helpers: `visibility.when(\"/path\")`, `visibility.unless(\"/path\")`, `visibility.eq(\"/path\", val)`, `visibility.and(cond1, cond2)`, `visibility.or(cond1, cond2)`.\n\n## Providers\n\n| Provider             | Purpose                                                                                                |\n| -------------------- | ------------------------------------------------------------------------------------------------------ |\n| `StateProvider`      | Share state across components (JSON Pointer paths). Accepts optional `store` prop for controlled mode. |\n| `ActionProvider`     | Handle actions dispatched via the event system                                                         |\n| `VisibilityProvider` | Enable conditional rendering based on state                                                            |\n| `ValidationProvider` | Form field validation                                                                                  |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider` (or `JSONUIProvider` / `createRenderer`) to use external state management (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{children}</StateProvider>;\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n## Dynamic Prop Expressions\n\nAny prop value can be a data-driven expression resolved by the renderer before components receive props:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads from state model (one-way read)\n- **`{ \"$bindState\": \"/path\" }`** - two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.\n- **`{ \"$bindItem\": \"field\" }`** - two-way binding to a repeat item field. Use inside repeat scopes.\n- **Filtered lists**: `repeat` plus an `$item` visible condition on the same container renders only matching items: `{ \"repeat\": { \"statePath\": \"/tasks\", \"key\": \"id\" }, \"visible\": { \"$item\": \"status\", \"eq\": \"todo\" }, \"children\": [\"task-card\"] }`. AND-composed `$state` conjuncts gate the container shell; `$item`/`$index` conjuncts filter items.\n- **Nested lists**: inside a repeat, use `{ \"repeat\": { \"statePath\": { \"$item\": \"comments\" }, \"key\": \"id\" } }` to iterate an array on the enclosing item.\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - conditional value\n- **`{ \"$template\": \"Hello, ${/name}!\" }`** - interpolates state values into strings\n- **`{ \"$computed\": \"fn\", \"args\": { ... } }`** - calls registered functions with resolved args\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nComponents do not use a `statePath` prop for two-way binding. Use `{ \"$bindState\": \"/path\" }` on the natural value prop instead.\n\nComponents receive already-resolved props. For two-way bound props, use the `useBoundProp` hook with the `bindings` map the renderer provides.\n\nRegister `$computed` functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:\n\n```tsx\n<JSONUIProvider\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n```\n\n## Event System\n\nComponents use `emit` to fire named events, or `on()` to get an event handle with metadata. The element's `on` field maps events to action bindings:\n\n```tsx\n// Simple event firing\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n),\n\n// Event handle with metadata (e.g. preventDefault)\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a href={props.href} onClick={(e) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    }}>{props.label}</a>\n  );\n},\n```\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } }\n}\n```\n\nThe `EventHandle` returned by `on()` has: `emit()`, `shouldPreventDefault` (boolean), and `bound` (boolean).\n\n## State Watchers\n\nElements can declare a `watch` field (top-level, sibling of type/props/children) to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\"]\n  },\n  \"watch\": { \"/form/country\": { \"action\": \"loadCities\" } },\n  \"children\": []\n}\n```\n\n## Built-in Actions\n\nThe `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the React schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in catalog `actions`:\n\n```json\n{ \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } }\n{ \"action\": \"pushState\", \"params\": { \"statePath\": \"/items\", \"value\": { \"text\": \"New\" } } }\n{ \"action\": \"removeState\", \"params\": { \"statePath\": \"/items\", \"index\": 0 } }\n{ \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } }\n```\n\n`validateForm` validates all registered fields and writes `{ valid, errors }` to state.\n\nNote: `statePath` in action params (e.g. `setState.statePath`) targets the mutation path. Two-way binding in component props uses `{ \"$bindState\": \"/path\" }` on the value prop, not `statePath`.\n\n## useBoundProp\n\nFor form components that need two-way binding, use `useBoundProp` with the `bindings` map the renderer provides when a prop uses `{ \"$bindState\": \"/path\" }` or `{ \"$bindItem\": \"field\" }`:\n\n```tsx\nimport { useBoundProp } from \"@json-render/react\";\n\nInput: ({ element, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(\n    element.props.value,\n    bindings?.value\n  );\n  return (\n    <input\n      value={value ?? \"\"}\n      onChange={(e) => setValue(e.target.value)}\n    />\n  );\n},\n```\n\n`useBoundProp(propValue, bindingPath)` returns `[value, setValue]`. The `value` is the resolved prop; `setValue` writes back to the bound state path (no-op if not bound).\n\n## BaseComponentProps\n\nFor building reusable component libraries not tied to a specific catalog (e.g. `@json-render/shadcn`):\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/react\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (\n  <div>{props.title}{children}</div>\n);\n```\n\n## defineRegistry\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it.\n\n## Key Exports\n\n| Export                  | Purpose                                                                                               |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `defineRegistry`        | Create a type-safe component registry from a catalog                                                  |\n| `Renderer`              | Render a spec using a registry                                                                        |\n| `schema`                | Element tree schema (includes built-in state actions: setState, pushState, removeState, validateForm) |\n| `useStateStore`         | Access state context                                                                                  |\n| `useStateValue`         | Get single value from state                                                                           |\n| `useBoundProp`          | Two-way binding for `$bindState`/`$bindItem` expressions                                              |\n| `useActions`            | Access actions context                                                                                |\n| `useAction`             | Get a single action dispatch function                                                                 |\n| `useOptionalValidation` | Non-throwing variant of useValidation (returns null if no provider)                                   |\n| `useUIStream`           | Stream specs from an API endpoint                                                                     |\n| `createStateStore`      | Create a framework-agnostic in-memory `StateStore`                                                    |\n| `StateStore`            | Interface for plugging in external state management                                                   |\n| `BaseComponentProps`    | Catalog-agnostic base type for reusable component libraries                                           |\n| `EventHandle`           | Event handle type (`emit`, `shouldPreventDefault`, `bound`)                                           |\n| `ComponentContext`      | Typed component context (catalog-aware)                                                               |\n\n<!-- File: skills/redux/SKILL.md (Tokens: ~492 | Category: anthropic-skill) -->\n---\nname: redux\ndescription: Redux adapter for json-render's StateStore interface. Use when integrating json-render with Redux or Redux Toolkit for state management via @json-render/redux.\n---\n\n# @json-render/redux\n\nRedux adapter for json-render's `StateStore` interface. Wire a Redux store (or Redux Toolkit slice) as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/redux @json-render/core @json-render/react redux\n# or with Redux Toolkit (recommended):\nnpm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit\n```\n\n## Usage\n\n```tsx\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Define a slice for json-render state\nconst uiSlice = createSlice({\n  name: \"ui\",\n  initialState: { count: 0 } as Record<string, unknown>,\n  reducers: {\n    replaceUiState: (_state, action) => action.payload,\n  },\n});\n\n// 2. Create the Redux store\nconst reduxStore = configureStore({\n  reducer: { ui: uiSlice.reducer },\n});\n\n// 3. Create the json-render StateStore adapter\nconst store = reduxStateStore({\n  store: reduxStore,\n  selector: (state) => state.ui,\n  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n});\n\n// 4. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Redux */}\n</StateProvider>\n```\n\n## API\n\n### `reduxStateStore(options)`\n\nCreates a `StateStore` backed by a Redux store.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `Store` | Yes | The Redux store instance |\n| `selector` | `(state) => StateModel` | Yes | Select the json-render slice from the Redux state tree. Use `(s) => s` if the entire state is the model. |\n| `dispatch` | `(nextState, store) => void` | Yes | Dispatch an action that replaces the selected slice with the next state |\n\nThe `dispatch` callback receives the full next state model and the Redux store.\n\n<!-- File: skills/remotion-best-practices/rules/3d.md (Tokens: ~622 | Category: anthropic-skill) -->\n---\nname: 3d\ndescription: 3D content in Remotion using Three.js and React Three Fiber.\nmetadata:\n  tags: 3d, three, threejs\n---\n\n# Using Three.js and React Three Fiber in Remotion\n\nFollow React Three Fiber and Three.js best practices.  \nOnly the following Remotion-specific rules need to be followed:\n\n## Prerequisites\n\nFirst, the `@remotion/three` package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/three # If project uses npm\nbunx remotion add @remotion/three # If project uses bun\nyarn remotion add @remotion/three # If project uses yarn\npnpm exec remotion add @remotion/three # If project uses pnpm\n```\n\n## Using ThreeCanvas\n\nYou MUST wrap 3D content in `<ThreeCanvas>` and include proper lighting.  \n`<ThreeCanvas>` MUST have a `width` and `height` prop.\n\n```tsx\nimport { ThreeCanvas } from \"@remotion/three\";\nimport { useVideoConfig } from \"remotion\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <ambientLight intensity={0.4} />\n  <directionalLight position={[5, 5, 5]} intensity={0.8} />\n  <mesh>\n    <sphereGeometry args={[1, 32, 32]} />\n    <meshStandardMaterial color=\"red\" />\n  </mesh>\n</ThreeCanvas>\n```\n\n## No animations not driven by `useCurrentFrame()`\n\nShaders, models etc MUST NOT animate by themselves.  \nNo animations are allowed unless they are driven by `useCurrentFrame()`.  \nOtherwise, it will cause flickering during rendering.  \n\nUsing `useFrame()` from `@react-three/fiber` is forbidden.\n\n## Animate using `useCurrentFrame()`\n\nUse `useCurrentFrame()` to perform animations.\n\n```tsx\nconst frame = useCurrentFrame();\nconst rotationY = frame * 0.02;\n\n<mesh rotation={[0, rotationY, 0]}>\n  <boxGeometry args={[2, 2, 2]} />\n  <meshStandardMaterial color=\"#4a9eff\" />\n</mesh>\n```\n\n## Using `<Sequence>` inside `<ThreeCanvas>`\n\nThe `layout` prop of any `<Sequence>` inside a `<ThreeCanvas>` must be set to `none`.\n\n```tsx\nimport { Sequence } from \"remotion\";\nimport { ThreeCanvas } from \"@remotion/three\";\n\nconst { width, height } = useVideoConfig();\n\n<ThreeCanvas width={width} height={height}>\n  <Sequence layout=\"none\">\n    <mesh>\n      <boxGeometry args={[2, 2, 2]} />\n      <meshStandardMaterial color=\"#4a9eff\" />\n    </mesh>\n  </Sequence>\n</ThreeCanvas>\n```\n\n<!-- File: skills/remotion-best-practices/rules/animations.md (Tokens: ~191 | Category: anthropic-skill) -->\n---\nname: animations\ndescription: Fundamental animation skills for Remotion\nmetadata:\n  tags: animations, transitions, frames, useCurrentFrame\n---\n\nAll animations MUST be driven by the `useCurrentFrame()` hook.  \nWrite animations in seconds and multiply them by the `fps` value from `useVideoConfig()`.\n\n```tsx\nimport { useCurrentFrame } from \"remotion\";\n\nexport const FadeIn = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {\n    extrapolateRight: 'clamp',\n  });\n \n  return (\n    <div style={{ opacity }}>Hello World!</div>\n  );\n};\n```\n\nCSS transitions or animations are FORBIDDEN - they will not render correctly.  \nTailwind animation class names are FORBIDDEN - they will not render correctly.\n\n<!-- File: skills/remotion-best-practices/rules/assets.md (Tokens: ~407 | Category: anthropic-skill) -->\n---\nname: assets\ndescription: Importing images, videos, audio, and fonts into Remotion\nmetadata:\n  tags: assets, staticFile, images, fonts, public\n---\n\n# Importing assets in Remotion\n\n## The public folder\n\nPlace assets in the `public/` folder at your project root.\n\n## Using staticFile()\n\nYou MUST use `staticFile()` to reference files from the `public/` folder:\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\nexport const MyComposition = () => {\n  return <Img src={staticFile('logo.png')} />;\n};\n```\n\nThe function returns an encoded URL that works correctly when deploying to subdirectories.\n\n## Using with components\n\n**Images:**\n\n```tsx\nimport {Img, staticFile} from 'remotion';\n\n<Img src={staticFile('photo.png')} />;\n```\n\n**Videos:**\n\n```tsx\nimport {Video} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Video src={staticFile('clip.mp4')} />;\n```\n\n**Audio:**\n\n```tsx\nimport {Audio} from '@remotion/media';\nimport {staticFile} from 'remotion';\n\n<Audio src={staticFile('music.mp3')} />;\n```\n\n**Fonts:**\n\n```tsx\nimport {staticFile} from 'remotion';\n\nconst fontFamily = new FontFace('MyFont', `url(${staticFile('font.woff2')})`);\nawait fontFamily.load();\ndocument.fonts.add(fontFamily);\n```\n\n## Remote URLs\n\nRemote URLs can be used directly without `staticFile()`:\n\n```tsx\n<Img src=\"https://example.com/image.png\" />\n<Video src=\"https://remotion.media/video.mp4\" />\n```\n\n## Important notes\n\n- Remotion components (`<Img>`, `<Video>`, `<Audio>`) ensure assets are fully loaded before rendering\n- Special characters in filenames (`#`, `?`, `&`) are automatically encoded\n\n<!-- File: skills/remotion-best-practices/rules/audio.md (Tokens: ~975 | Category: anthropic-skill) -->\n---\nname: audio\ndescription: Using audio and sound in Remotion - importing, trimming, volume, speed, pitch\nmetadata:\n  tags: audio, media, trim, volume, speed, loop, pitch, mute, sound, sfx\n---\n\n# Using audio in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/media package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/media\n```\n\n## Importing Audio\n\nUse `<Audio>` from `@remotion/media` to add audio to your composition.\n\n```tsx\nimport { Audio } from \"@remotion/media\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Audio src={staticFile(\"audio.mp3\")} />;\n};\n```\n\nRemote URLs are also supported:\n\n```tsx\n<Audio src=\"https://remotion.media/audio.mp3\" />\n```\n\nBy default, audio plays from the start, at full volume and full length.\nMultiple audio tracks can be layered by adding multiple `<Audio>` components.\n\n## Trimming\n\nUse `trimBefore` and `trimAfter` to remove portions of the audio. Values are in frames.\n\n```tsx\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    trimBefore={2 * fps} // Skip the first 2 seconds\n    trimAfter={10 * fps} // End at the 10 second mark\n  />\n);\n```\n\nThe audio still starts playing at the beginning of the composition - only the specified portion is played.\n\n## Delaying\n\nWrap the audio in a `<Sequence>` to delay when it starts:\n\n```tsx\nimport { Sequence, staticFile } from \"remotion\";\nimport { Audio } from \"@remotion/media\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Sequence from={1 * fps}>\n    <Audio src={staticFile(\"audio.mp3\")} />\n  </Sequence>\n);\n```\n\nThe audio will start playing after 1 second.\n\n## Volume\n\nSet a static volume (0 to 1):\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} volume={0.5} />\n```\n\nOr use a callback for dynamic volume based on the current frame:\n\n```tsx\nimport { interpolate } from \"remotion\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    volume={(f) =>\n      interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: \"clamp\" })\n    }\n  />\n);\n```\n\nThe value of `f` starts at 0 when the audio begins to play, not the composition frame.\n\n## Muting\n\nUse `muted` to silence the audio. It can be set dynamically:\n\n```tsx\nconst frame = useCurrentFrame();\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Audio\n    src={staticFile(\"audio.mp3\")}\n    muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s\n  />\n);\n```\n\n## Speed\n\nUse `playbackRate` to change the playback speed:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={2} /> {/* 2x speed */}\n<Audio src={staticFile(\"audio.mp3\")} playbackRate={0.5} /> {/* Half speed */}\n```\n\nReverse playback is not supported.\n\n## Looping\n\nUse `loop` to loop the audio indefinitely:\n\n```tsx\n<Audio src={staticFile(\"audio.mp3\")} loop />\n```\n\nUse `loopVolumeCurveBehavior` to control how the frame count behaves when looping:\n\n- `\"repeat\"`: Frame count resets to 0 each loop (default)\n- `\"extend\"`: Frame count continues incrementing\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  loop\n  loopVolumeCurveBehavior=\"extend\"\n  volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops\n/>\n```\n\n## Pitch\n\nUse `toneFrequency` to adjust the pitch without affecting speed. Values range from 0.01 to 2:\n\n```tsx\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={1.5} // Higher pitch\n/>\n<Audio\n  src={staticFile(\"audio.mp3\")}\n  toneFrequency={0.8} // Lower pitch\n/>\n```\n\nPitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the `<Player />`.\n\n<!-- File: skills/remotion-best-practices/rules/calculate-metadata.md (Tokens: ~662 | Category: anthropic-skill) -->\n---\nname: calculate-metadata\ndescription: Dynamically set composition duration, dimensions, and props\nmetadata:\n  tags: calculateMetadata, duration, dimensions, props, dynamic\n---\n\n# Using calculateMetadata\n\nUse `calculateMetadata` on a `<Composition>` to dynamically set duration, dimensions, and transform props before rendering.\n\n```tsx\n<Composition id=\"MyComp\" component={MyComponent} durationInFrames={300} fps={30} width={1920} height={1080} defaultProps={{videoSrc: 'https://remotion.media/video.mp4'}} calculateMetadata={calculateMetadata} />\n```\n\n## Setting duration based on a video\n\nUse the `getMediaMetadata()` function from the mediabunny/metadata skill to get the video duration:\n\n```tsx\nimport {CalculateMetadataFunction} from 'remotion';\nimport {getMediaMetadata} from '../get-media-metadata';\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n  };\n};\n```\n\n## Matching dimensions of a video\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const {durationInSeconds, dimensions} = await getMediaMetadata(props.videoSrc);\n\n  return {\n    durationInFrames: Math.ceil(durationInSeconds * 30),\n    width: dimensions?.width ?? 1920,\n    height: dimensions?.height ?? 1080,\n  };\n};\n```\n\n## Setting duration based on multiple videos\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  const metadataPromises = props.videos.map((video) => getMediaMetadata(video.src));\n  const allMetadata = await Promise.all(metadataPromises);\n\n  const totalDuration = allMetadata.reduce((sum, meta) => sum + meta.durationInSeconds, 0);\n\n  return {\n    durationInFrames: Math.ceil(totalDuration * 30),\n  };\n};\n```\n\n## Setting a default outName\n\nSet the default output filename based on props:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props}) => {\n  return {\n    defaultOutName: `video-${props.id}.mp4`,\n  };\n};\n```\n\n## Transforming props\n\nFetch data or transform props before rendering:\n\n```tsx\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({props, abortSignal}) => {\n  const response = await fetch(props.dataUrl, {signal: abortSignal});\n  const data = await response.json();\n\n  return {\n    props: {\n      ...props,\n      fetchedData: data,\n    },\n  };\n};\n```\n\nThe `abortSignal` cancels stale requests when props change in the Studio.\n\n## Return value\n\nAll fields are optional. Returned values override the `<Composition>` props:\n\n- `durationInFrames`: Number of frames\n- `width`: Composition width in pixels\n- `height`: Composition height in pixels\n- `fps`: Frames per second\n- `props`: Transformed props passed to the component\n- `defaultOutName`: Default output filename\n- `defaultCodec`: Default codec for rendering\n\n<!-- File: skills/remotion-best-practices/rules/can-decode.md (Tokens: ~401 | Category: anthropic-skill) -->\n---\nname: can-decode\ndescription: Check if a video can be decoded by the browser using Mediabunny\nmetadata:\n  tags: decode, validation, video, audio, compatibility, browser\n---\n\n# Checking if a video can be decoded\n\nUse Mediabunny to check if a video can be decoded by the browser before attempting to play it.\n\n## The `canDecode()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport { Input, ALL_FORMATS, UrlSource } from \"mediabunny\";\n\nexport const canDecode = async (src: string) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src, {\n      getRetryDelay: () => null,\n    }),\n  });\n\n  try {\n    await input.getFormat();\n  } catch {\n    return false;\n  }\n\n  const videoTrack = await input.getPrimaryVideoTrack();\n  if (videoTrack && !(await videoTrack.canDecode())) {\n    return false;\n  }\n\n  const audioTrack = await input.getPrimaryAudioTrack();\n  if (audioTrack && !(await audioTrack.canDecode())) {\n    return false;\n  }\n\n  return true;\n};\n```\n\n## Usage\n\n```tsx\nconst src = \"https://remotion.media/video.mp4\";\nconst isDecodable = await canDecode(src);\n\nif (isDecodable) {\n  console.log(\"Video can be decoded\");\n} else {\n  console.log(\"Video cannot be decoded by this browser\");\n}\n```\n\n## Using with Blob\n\nFor file uploads or drag-and-drop, use `BlobSource`:\n\n```tsx\nimport { Input, ALL_FORMATS, BlobSource } from \"mediabunny\";\n\nexport const canDecodeBlob = async (blob: Blob) => {\n  const input = new Input({\n    formats: ALL_FORMATS,\n    source: new BlobSource(blob),\n  });\n\n  // Same validation logic as above\n};\n```\n\n<!-- File: skills/remotion-best-practices/rules/charts.md (Tokens: ~432 | Category: anthropic-skill) -->\n---\nname: charts\ndescription: Chart and data visualization patterns for Remotion. Use when creating bar charts, pie charts, histograms, progress bars, or any data-driven animations.\nmetadata:\n  tags: charts, data, visualization, bar-chart, pie-chart, graphs\n---\n\n# Charts in Remotion\n\nYou can create bar charts in Remotion by using regular React code - HTML and SVG is allowed, as well as D3.js.\n\n## No animations not powered by `useCurrentFrame()`\n\nDisable all animations by third party libraries.  \nThey will cause flickering during rendering.  \nInstead, drive all animations from `useCurrentFrame()`.\n\n## Bar Chart Animations\n\nSee [Bar Chart Example](assets/charts/bar-chart.tsx) for a basic example implmentation.\n\n### Staggered Bars\n\nYou can animate the height of the bars and stagger them like this:\n\n```tsx\nconst STAGGER_DELAY = 5;\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst bars = data.map((item, i) => {\n  const delay = i * STAGGER_DELAY;\n  const height = spring({\n    frame,\n    fps,\n    delay,\n    config: {damping: 200},\n  });\n  return <div style={{height: height * item.value}} />;\n});\n```\n\n## Pie Chart Animation\n\nAnimate segments using stroke-dashoffset, starting from 12 o'clock.\n\n```tsx\nconst frame = useCurrentFrame();\nconst {fps} = useVideoConfig();\n\nconst progress = interpolate(frame, [0, 100], [0, 1]);\n\nconst circumference = 2 * Math.PI * radius;\nconst segmentLength = (value / total) * circumference;\nconst offset = interpolate(progress, [0, 1], [segmentLength, 0]);\n\n<circle r={radius} cx={center} cy={center} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} transform={`rotate(-90 ${center} ${center})`} />;\n```\n\n<!-- File: skills/remotion-best-practices/rules/compositions.md (Tokens: ~882 | Category: anthropic-skill) -->\n---\nname: compositions\ndescription: Defining compositions, stills, folders, default props and dynamic metadata\nmetadata:\n  tags: composition, still, folder, props, metadata\n---\n\nA `<Composition>` defines the component, width, height, fps and duration of a renderable video.\n\nIt normally is placed in the `src/Root.tsx` file.\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return <Composition id=\"MyComposition\" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;\n};\n```\n\n## Default Props\n\nPass `defaultProps` to provide initial values for your component.  \nValues must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).\n\n```tsx\nimport {Composition} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100}\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={\n        {\n          title: 'Hello World',\n          color: '#ff0000',\n        } satisfies MyCompositionProps\n      }\n    />\n  );\n};\n```\n\nUse `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.\n\n## Folders\n\nUse `<Folder>` to organize compositions in the sidebar.  \nFolder names can only contain letters, numbers, and hyphens.\n\n```tsx\nimport {Composition, Folder} from 'remotion';\n\nexport const RemotionRoot = () => {\n  return (\n    <>\n      <Folder name=\"Marketing\">\n        <Composition id=\"Promo\" /* ... */ />\n        <Composition id=\"Ad\" /* ... */ />\n      </Folder>\n      <Folder name=\"Social\">\n        <Folder name=\"Instagram\">\n          <Composition id=\"Story\" /* ... */ />\n          <Composition id=\"Reel\" /* ... */ />\n        </Folder>\n      </Folder>\n    </>\n  );\n};\n```\n\n## Stills\n\nUse `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.\n\n```tsx\nimport {Still} from 'remotion';\nimport {Thumbnail} from './Thumbnail';\n\nexport const RemotionRoot = () => {\n  return <Still id=\"Thumbnail\" component={Thumbnail} width={1280} height={720} />;\n};\n```\n\n## Calculate Metadata\n\nUse `calculateMetadata` to make dimensions, duration, or props dynamic based on data.\n\n```tsx\nimport {Composition, CalculateMetadataFunction} from 'remotion';\nimport {MyComposition, MyCompositionProps} from './MyComposition';\n\nconst calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {\n  const data = await fetch(`https://api.example.com/video/${props.videoId}`, {\n    signal: abortSignal,\n  }).then((res) => res.json());\n\n  return {\n    durationInFrames: Math.ceil(data.duration * 30),\n    props: {\n      ...props,\n      videoUrl: data.url,\n    },\n  };\n};\n\nexport const RemotionRoot = () => {\n  return (\n    <Composition\n      id=\"MyComposition\"\n      component={MyComposition}\n      durationInFrames={100} // Placeholder, will be overridden\n      fps={30}\n      width={1080}\n      height={1080}\n      defaultProps={{videoId: 'abc123'}}\n      calculateMetadata={calculateMetadata}\n    />\n  );\n};\n```\n\nThe function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.\n\n## Nesting compositions within another\n\nTo add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.\n\n```tsx\n<AbsoluteFill>\n  <Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>\n    <CompositionComponent />\n  </Sequence>\n</AbsoluteFill>\n```\n\n<!-- File: skills/remotion-best-practices/rules/display-captions.md (Tokens: ~1303 | Category: anthropic-skill) -->\n---\nname: display-captions\ndescription: Displaying captions in Remotion with TikTok-style pages and word highlighting\nmetadata:\n  tags: captions, subtitles, display, tiktok, highlight\n---\n\n# Displaying captions in Remotion\n\nThis guide explains how to display captions in Remotion, assuming you already have captions in the [`Caption`](https://www.remotion.dev/docs/captions/caption) format.\n\n## Prerequisites\n\nRead [Transcribing audio](transcribe-captions.md) for how to generate captions.\n\nFirst, the [`@remotion/captions`](https://www.remotion.dev/docs/captions) package needs to be installed.\nIf it is not installed, use the following command:\n\n```bash\nnpx remotion add @remotion/captions\n```\n\n## Fetching captions\n\nFirst, fetch your captions JSON file. Use [`useDelayRender()`](https://www.remotion.dev/docs/use-delay-render) to hold the render until the captions are loaded:\n\n```tsx\nimport { useState, useEffect, useCallback } from \"react\";\nimport { AbsoluteFill, staticFile, useDelayRender } from \"remotion\";\nimport type { Caption } from \"@remotion/captions\";\n\nexport const MyComponent: React.FC = () => {\n  const [captions, setCaptions] = useState<Caption[] | null>(null);\n  const { delayRender, continueRender, cancelRender } = useDelayRender();\n  const [handle] = useState(() => delayRender());\n\n  const fetchCaptions = useCallback(async () => {\n    try {\n      // Assuming captions.json is in the public/ folder.\n      const response = await fetch(staticFile(\"captions123.json\"));\n      const data = await response.json();\n      setCaptions(data);\n      continueRender(handle);\n    } catch (e) {\n      cancelRender(e);\n    }\n  }, [continueRender, cancelRender, handle]);\n\n  useEffect(() => {\n    fetchCaptions();\n  }, [fetchCaptions]);\n\n  if (!captions) {\n    return null;\n  }\n\n  return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;\n};\n```\n\n## Creating pages\n\nUse `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:\n\n```tsx\nimport { useMemo } from \"react\";\nimport { createTikTokStyleCaptions } from \"@remotion/captions\";\nimport type { Caption } from \"@remotion/captions\";\n\n// How often captions should switch (in milliseconds)\n// Higher values = more words per page\n// Lower values = fewer words (more word-by-word)\nconst SWITCH_CAPTIONS_EVERY_MS = 1200;\n\nconst { pages } = useMemo(() => {\n  return createTikTokStyleCaptions({\n    captions,\n    combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,\n  });\n}, [captions]);\n```\n\n## Rendering with Sequences\n\nMap over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:\n\n```tsx\nimport { Sequence, useVideoConfig, AbsoluteFill } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst CaptionedContent: React.FC = () => {\n  const { fps } = useVideoConfig();\n\n  return (\n    <AbsoluteFill>\n      {pages.map((page, index) => {\n        const nextPage = pages[index + 1] ?? null;\n        const startFrame = (page.startMs / 1000) * fps;\n        const endFrame = Math.min(\n          nextPage ? (nextPage.startMs / 1000) * fps : Infinity,\n          startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,\n        );\n        const durationInFrames = endFrame - startFrame;\n\n        if (durationInFrames <= 0) {\n          return null;\n        }\n\n        return (\n          <Sequence\n            key={index}\n            from={startFrame}\n            durationInFrames={durationInFrames}\n          >\n            <CaptionPage page={page} />\n          </Sequence>\n        );\n      })}\n    </AbsoluteFill>\n  );\n};\n```\n\n## White-space preservation\n\nThe captions are whitespace sensitive. You should include spaces in the `text` field before each word. Use `whiteSpace: \"pre\"` to preserve the whitespace in the captions.\n\n## Separate component for captions\n\nPut captioning logic in a separate component.  \nMake a new file for it.\n\n## Word highlighting\n\nA caption page contains `tokens` which you can use to highlight the currently spoken word:\n\n```tsx\nimport { AbsoluteFill, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport type { TikTokPage } from \"@remotion/captions\";\n\nconst HIGHLIGHT_COLOR = \"#39E508\";\n\nconst CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Current time relative to the start of the sequence\n  const currentTimeMs = (frame / fps) * 1000;\n  // Convert to absolute time by adding the page start\n  const absoluteTimeMs = page.startMs + currentTimeMs;\n\n  return (\n    <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n      <div style={{ fontSize: 80, fontWeight: \"bold\", whiteSpace: \"pre\" }}>\n        {page.tokens.map((token) => {\n          const isActive =\n            token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;\n\n          return (\n            <span\n              key={token.fromMs}\n              style={{ color: isActive ? HIGHLIGHT_COLOR : \"white\" }}\n            >\n              {token.text}\n            </span>\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n```\n\n## Display captions alongside video content\n\nBy default, put the captions alongside the video content, so the captions are in sync.  \nFor each video, make a new captions JSON file.\n\n```tsx\n<AbsoluteFill>\n  <Video src={staticFile(\"video.mp4\")} />\n  <CaptionPage page={page} />\n</AbsoluteFill>\n```\n\n<!-- File: skills/remotion-best-practices/rules/extract-frames.md (Tokens: ~1298 | Category: anthropic-skill) -->\n---\nname: extract-frames\ndescription: Extract frames from videos at specific timestamps using Mediabunny\nmetadata:\n  tags: frames, extract, video, thumbnail, filmstrip, canvas\n---\n\n# Extracting frames from videos\n\nUse Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.\n\n## The `extractFrames()` function\n\nThis function can be copy-pasted into any project.\n\n```tsx\nimport {\n  ALL_FORMATS,\n  Input,\n  UrlSource,\n  VideoSample,\n  VideoSampleSink,\n} from \"mediabunny\";\n\ntype Options = {\n  track: { width: number; height: number };\n  container: string;\n  durationInSeconds: number | null;\n};\n\nexport type ExtractFramesTimestampsInSecondsFn = (\n  options: Options\n) => Promise<number[]> | number[];\n\nexport type ExtractFramesProps = {\n  src: string;\n  timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;\n  onVideoSample: (sample: VideoSample) => void;\n  signal?: AbortSignal;\n};\n\nexport async function extractFrames({\n  src,\n  timestampsInSeconds,\n  onVideoSample,\n  signal,\n}: ExtractFramesProps): Promise<void> {\n  using input = new Input({\n    formats: ALL_FORMATS,\n    source: new UrlSource(src),\n  });\n\n  const [durationInSeconds, format, videoTrack] = await Promise.all([\n    input.computeDuration(),\n    input.getFormat(),\n    input.getPrimaryVideoTrack(),\n  ]);\n\n  if (!videoTrack) {\n    throw new Error(\"No video track found in the input\");\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const timestamps =\n    typeof timestampsInSeconds === \"function\"\n      ? await timestampsInSeconds({\n          track: {\n            width: videoTrack.displayWidth,\n            height: videoTrack.displayHeight,\n          },\n          container: format.name,\n          durationInSeconds,\n        })\n      : timestampsInSeconds;\n\n  if (timestamps.length === 0) {\n    return;\n  }\n\n  if (signal?.aborted) {\n    throw new Error(\"Aborted\");\n  }\n\n  const sink = new VideoSampleSink(videoTrack);\n\n  for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {\n    if (signal?.aborted) {\n      break;\n    }\n\n    if (!videoSample) {\n      continue;\n    }\n\n    onVideoSample(videoSample);\n  }\n}\n```\n\n## Basic usage\n\nExtract frames at specific timestamps:\n\n```tsx\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: [0, 1, 2, 3, 4],\n  onVideoSample: (sample) => {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Creating a filmstrip\n\nUse a callback function to dynamically calculate timestamps based on video metadata:\n\n```tsx\nconst canvasWidth = 500;\nconst canvasHeight = 80;\nconst fromSeconds = 0;\nconst toSeconds = 10;\n\nawait extractFrames({\n  src: \"https://remotion.media/video.mp4\",\n  timestampsInSeconds: async ({ track, durationInSeconds }) => {\n    const aspectRatio = track.width / track.height;\n    const amountOfFramesFit = Math.ceil(\n      canvasWidth / (canvasHeight * aspectRatio)\n    );\n    const segmentDuration = toSeconds - fromSeconds;\n    const timestamps: number[] = [];\n\n    for (let i = 0; i < amountOfFramesFit; i++) {\n      timestamps.push(\n        fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)\n      );\n    }\n\n    return timestamps;\n  },\n  onVideoSample: (sample) => {\n    console.log(`Frame at ${sample.timestamp}s`);\n\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = sample.displayWidth;\n    canvas.height = sample.displayHeight;\n    const ctx = canvas.getContext(\"2d\");\n    sample.draw(ctx!, 0, 0);\n  },\n});\n```\n\n## Cancellation with AbortSignal\n\nCancel frame extraction after a timeout:\n\n```tsx\nconst controller = new AbortController();\n\nsetTimeout(() => controller.abort(), 5000);\n\ntry {\n  await extractFrames({\n    src: \"https://remotion.media/video.mp4\",\n    timestampsInSeconds: [0, 1, 2, 3, 4],\n    onVideoSample: (sample) => {\n      using frame = sample;\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = frame.displayWidth;\n      canvas.height = frame.displayHeight;\n      const ctx = canvas.getContext(\"2d\");\n      frame.draw(ctx!, 0, 0);\n    },\n    signal: controller.signal,\n  });\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n\n## Timeout with Promise.race\n\n```tsx\nconst controller = new AbortController();\n\nconst timeoutPromise = new Promise<never>((_, reject) => {\n  const timeoutId = setTimeout(() => {\n    controller.abort();\n    reject(new Error(\"Frame extraction timed out after 10 seconds\"));\n  }, 10000);\n\n  controller.signal.addEventListener(\"abort\", () => clearTimeout(timeoutId), {\n    once: true,\n  });\n});\n\ntry {\n  await Promise.race([\n    extractFrames({\n      src: \"https://remotion.media/video.mp4\",\n      timestampsInSeconds: [0, 1, 2, 3, 4],\n      onVideoSample: (sample) => {\n        using frame = sample;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = frame.displayWidth;\n        canvas.height = frame.displayHeight;\n        const ctx = canvas.getContext(\"2d\");\n        frame.draw(ctx!, 0, 0);\n      },\n      signal: controller.signal,\n    }),\n    timeoutPromise,\n  ]);\n\n  console.log(\"Frame extraction complete!\");\n} catch (error) {\n  console.error(\"Frame extraction was aborted or failed:\", error);\n}\n```\n\n<!-- ... +69 more modular skills available via /api/skills/vercel-labs/json-render?path={path} -->\n</yakaai_skills>"}