[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in the repository](https://github.com/changesets/changesets).\n\n## Adding a changeset\n\nTo add a changeset, run `pnpm changeset` in the root of the repository. This will prompt you to select\nwhich packages have changed and what type of version bump (major, minor, or patch) should be applied.\n\nAll `@json-render/*` packages are versioned together -- a changeset for any one of them will bump all\npackages to the same version.\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.1/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [\n    [\n      \"@json-render/core\",\n      \"@json-render/react\",\n      \"@json-render/react-email\",\n      \"@json-render/react-pdf\",\n      \"@json-render/shadcn\",\n      \"@json-render/react-native\",\n      \"@json-render/remotion\",\n      \"@json-render/codegen\",\n      \"@json-render/zustand\",\n      \"@json-render/redux\",\n      \"@json-render/jotai\",\n      \"@json-render/vue\",\n      \"@json-render/xstate\",\n      \"@json-render/image\",\n      \"@json-render/mcp\",\n      \"@json-render/svelte\",\n      \"@json-render/solid\",\n      \"@json-render/react-three-fiber\",\n      \"@json-render/yaml\"\n    ]\n  ],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"privatePackages\": {\n    \"version\": true,\n    \"tag\": false\n  }\n}\n"
  },
  {
    "path": ".cursor/mcp.json",
    "content": "{\n  \"mcpServers\": {\n    \"json-render\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"examples/mcp/server.ts\", \"--stdio\"]\n    }\n  }\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm lint\n\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n      - run: pnpm install --frozen-lockfile\n      - name: Build packages\n        run: pnpm turbo run build --filter='./packages/*'\n      - run: pnpm test\n\n  typecheck:\n    name: Type Check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm type-check\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\nconcurrency: ${{ github.workflow }}-${{ github.ref }}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Create Release Pull Request or Publish\n        uses: changesets/action@v1\n        with:\n          version: pnpm ci:version\n          publish: pnpm ci:publish\n          title: \"chore: version packages\"\n          commit: \"chore: version packages\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\n.pnp\n.pnp.js\n.pnpm-store/\n\n# Local env files\n.env*\n!.env.example\n\n# Testing\ncoverage\n\n# Turbo\n.turbo\n\n# Vercel\n.vercel\n\n# Expo\n.expo/\n\n# Build Outputs\n.next/\nout/\nbuild\ndist\n*.tsbuildinfo\n.svelte-kit/\n\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Misc\n.DS_Store\n*.pem\n\n# opensrc - source code for packages\nopensrc/\n\n# Stripe apps (generated from template + build artifacts)\nexamples/stripe-app/*/stripe-app.json\nexamples/stripe-app/*/.build\nexamples/stripe-app/*/yarn.lock\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm lint-staged\n"
  },
  {
    "path": ".npmrc",
    "content": ""
  },
  {
    "path": ".vscode/mcp.json",
    "content": "{\n  \"servers\": {\n    \"json-render\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"examples/mcp/server.ts\", \"--stdio\"]\n    }\n  }\n}"
  },
  {
    "path": "AGENTS.md",
    "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- 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## Releases\n\nThis monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.\n\n### Fixed version group\n\nAll public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest.\n\n### Preparing a release\n\nWhen asked to prepare a release (e.g. \"prepare v0.12.0\"):\n\n1. **Create a changeset file** at `.changeset/v0-<N>-release.md` following the existing pattern:\n   - YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases)\n   - A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change\n   - Always list `@json-render/core` plus any packages with actual code changes\n2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically\n3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file\n4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest\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/app/(main)/docs/api/<name>/page.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\n\n### CI scripts\n\n- `pnpm changeset` — interactively create a new changeset\n- `pnpm ci:version` — run `changeset version` + lockfile update (CI only)\n- `pnpm 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"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1.  Definitions.\n\n    \"License\" shall mean the terms and conditions for use, reproduction,\n    and distribution as defined by Sections 1 through 9 of this document.\n\n    \"Licensor\" shall mean the copyright owner or entity authorized by\n    the copyright owner that is granting the License.\n\n    \"Legal Entity\" shall mean the union of the acting entity and all\n    other entities that control, are controlled by, or are under common\n    control with that entity. For the purposes of this definition,\n    \"control\" means (i) the power, direct or indirect, to cause the\n    direction or management of such entity, whether by contract or\n    otherwise, or (ii) ownership of fifty percent (50%) or more of the\n    outstanding shares, or (iii) beneficial ownership of such entity.\n\n    \"You\" (or \"Your\") shall mean an individual or Legal Entity\n    exercising permissions granted by this License.\n\n    \"Source\" form shall mean the preferred form for making modifications,\n    including but not limited to software source code, documentation\n    source, and configuration files.\n\n    \"Object\" form shall mean any form resulting from mechanical\n    transformation or translation of a Source form, including but\n    not limited to compiled object code, generated documentation,\n    and conversions to other media types.\n\n    \"Work\" shall mean the work of authorship, whether in Source or\n    Object form, made available under the License, as indicated by a\n    copyright notice that is included in or attached to the work\n    (an example is provided in the Appendix below).\n\n    \"Derivative Works\" shall mean any work, whether in Source or Object\n    form, that is based on (or derived from) the Work and for which the\n    editorial revisions, annotations, elaborations, or other modifications\n    represent, as a whole, an original work of authorship. For the purposes\n    of this License, Derivative Works shall not include works that remain\n    separable from, or merely link (or bind by name) to the interfaces of,\n    the Work and Derivative Works thereof.\n\n    \"Contribution\" shall mean any work of authorship, including\n    the original version of the Work and any modifications or additions\n    to that Work or Derivative Works thereof, that is intentionally\n    submitted to Licensor for inclusion in the Work by the copyright owner\n    or by an individual or Legal Entity authorized to submit on behalf of\n    the copyright owner. For the purposes of this definition, \"submitted\"\n    means any form of electronic, verbal, or written communication sent\n    to the Licensor or its representatives, including but not limited to\n    communication on electronic mailing lists, source code control systems,\n    and issue tracking systems that are managed by, or on behalf of, the\n    Licensor for the purpose of discussing and improving the Work, but\n    excluding communication that is conspicuously marked or otherwise\n    designated in writing by the copyright owner as \"Not a Contribution.\"\n\n    \"Contributor\" shall mean Licensor and any individual or Legal Entity\n    on behalf of whom a Contribution has been received by Licensor and\n    subsequently incorporated within the Work.\n\n2.  Grant of Copyright License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    copyright license to reproduce, prepare Derivative Works of,\n    publicly display, publicly perform, sublicense, and distribute the\n    Work and such Derivative Works in Source or Object form.\n\n3.  Grant of Patent License. Subject to the terms and conditions of\n    this License, each Contributor hereby grants to You a perpetual,\n    worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n    (except as stated in this section) patent license to make, have made,\n    use, offer to sell, sell, import, and otherwise transfer the Work,\n    where such license applies only to those patent claims licensable\n    by such Contributor that are necessarily infringed by their\n    Contribution(s) alone or by combination of their Contribution(s)\n    with the Work to which such Contribution(s) was submitted. If You\n    institute patent litigation against any entity (including a\n    cross-claim or counterclaim in a lawsuit) alleging that the Work\n    or a Contribution incorporated within the Work constitutes direct\n    or contributory patent infringement, then any patent licenses\n    granted to You under this License for that Work shall terminate\n    as of the date such litigation is filed.\n\n4.  Redistribution. You may reproduce and distribute copies of the\n    Work or Derivative Works thereof in any medium, with or without\n    modifications, and in Source or Object form, provided that You\n    meet the following conditions:\n\n    (a) You must give any other recipients of the Work or\n    Derivative Works a copy of this License; and\n\n    (b) You must cause any modified files to carry prominent notices\n    stating that You changed the files; and\n\n    (c) You must retain, in the Source form of any Derivative Works\n    that You distribute, all copyright, patent, trademark, and\n    attribution notices from the Source form of the Work,\n    excluding those notices that do not pertain to any part of\n    the Derivative Works; and\n\n    (d) If the Work includes a \"NOTICE\" text file as part of its\n    distribution, then any Derivative Works that You distribute must\n    include a readable copy of the attribution notices contained\n    within such NOTICE file, excluding those notices that do not\n    pertain to any part of the Derivative Works, in at least one\n    of the following places: within a NOTICE text file distributed\n    as part of the Derivative Works; within the Source form or\n    documentation, if provided along with the Derivative Works; or,\n    within a display generated by the Derivative Works, if and\n    wherever such third-party notices normally appear. The contents\n    of the NOTICE file are for informational purposes only and\n    do not modify the License. You may add Your own attribution\n    notices within Derivative Works that You distribute, alongside\n    or as an addendum to the NOTICE text from the Work, provided\n    that such additional attribution notices cannot be construed\n    as modifying the License.\n\n    You may add Your own copyright statement to Your modifications and\n    may provide additional or different license terms and conditions\n    for use, reproduction, or distribution of Your modifications, or\n    for any such Derivative Works as a whole, provided Your use,\n    reproduction, and distribution of the Work otherwise complies with\n    the conditions stated in this License.\n\n5.  Submission of Contributions. Unless You explicitly state otherwise,\n    any Contribution intentionally submitted for inclusion in the Work\n    by You to the Licensor shall be under the terms and conditions of\n    this License, without any additional terms or conditions.\n    Notwithstanding the above, nothing herein shall supersede or modify\n    the terms of any separate license agreement you may have executed\n    with Licensor regarding such Contributions.\n\n6.  Trademarks. This License does not grant permission to use the trade\n    names, trademarks, service marks, or product names of the Licensor,\n    except as required for reasonable and customary use in describing the\n    origin of the Work and reproducing the content of the NOTICE file.\n\n7.  Disclaimer of Warranty. Unless required by applicable law or\n    agreed to in writing, Licensor provides the Work (and each\n    Contributor provides its Contributions) on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n    implied, including, without limitation, any warranties or conditions\n    of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n    PARTICULAR PURPOSE. You are solely responsible for determining the\n    appropriateness of using or redistributing the Work and assume any\n    risks associated with Your exercise of permissions under this License.\n\n8.  Limitation of Liability. In no event and under no legal theory,\n    whether in tort (including negligence), contract, or otherwise,\n    unless required by applicable law (such as deliberate and grossly\n    negligent acts) or agreed to in writing, shall any Contributor be\n    liable to You for damages, including any direct, indirect, special,\n    incidental, or consequential damages of any character arising as a\n    result of this License or out of the use or inability to use the\n    Work (including but not limited to damages for loss of goodwill,\n    work stoppage, computer failure or malfunction, or any and all\n    other commercial damages or losses), even if such Contributor\n    has been advised of the possibility of such damages.\n\n9.  Accepting Warranty or Additional Liability. While redistributing\n    the Work or Derivative Works thereof, You may choose to offer,\n    and charge a fee for, acceptance of support, warranty, indemnity,\n    or other liability obligations and/or rights consistent with this\n    License. However, in accepting such obligations, You may act only\n    on Your own behalf and on Your sole responsibility, not on behalf\n    of any other Contributor, and only if You agree to indemnify,\n    defend, and hold each Contributor harmless for any liability\n    incurred by, or claims asserted against, such Contributor by reason\n    of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\nCopyright 2025 Vercel Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# json-render\n\n**The Generative UI framework.**\n\nGenerate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.\n\n```bash\n# for React\nnpm install @json-render/core @json-render/react\n# for React with pre-built shadcn/ui components\nnpm install @json-render/shadcn\n# or for React Native\nnpm install @json-render/core @json-render/react-native\n# or for video\nnpm install @json-render/core @json-render/remotion\n# or for PDF documents\nnpm install @json-render/core @json-render/react-pdf\n# or for HTML email\nnpm install @json-render/core @json-render/react-email @react-email/components @react-email/render\n# or for Vue\nnpm install @json-render/core @json-render/vue\n# or for Svelte\nnpm install @json-render/core @json-render/svelte\n# or for SolidJS\nnpm install @json-render/core @json-render/solid\n# or for 3D scenes\nnpm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three\n```\n\n## Why json-render?\n\njson-render is a **Generative UI** framework: AI generates interfaces from natural language prompts, constrained to components you define. You set the guardrails, AI generates within them:\n\n- **Guardrailed** - AI can only use components in your catalog\n- **Predictable** - JSON output matches your schema, every time\n- **Fast** - Stream and render progressively as the model responds\n- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog\n- **Batteries Included** - 36 pre-built shadcn/ui components ready to use\n\n## Quick Start\n\n### 1. Define Your Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string() }),\n      description: \"A card container\",\n    },\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        format: z.enum([\"currency\", \"percent\", \"number\"]).nullable(),\n      }),\n      description: \"Display a metric value\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n      }),\n      description: \"Clickable button\",\n    },\n  },\n  actions: {\n    export_report: { description: \"Export dashboard to PDF\" },\n    refresh_data: { description: \"Refresh all metrics\" },\n  },\n});\n```\n\n### 2. Define Your Components\n\n```tsx\nimport { defineRegistry, Renderer } from \"@json-render/react\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h3>{props.title}</h3>\n        {children}\n      </div>\n    ),\n    Metric: ({ props }) => (\n      <div className=\"metric\">\n        <span>{props.label}</span>\n        <span>{format(props.value, props.format)}</span>\n      </div>\n    ),\n    Button: ({ props, emit }) => (\n      <button onClick={() => emit(\"press\")}>{props.label}</button>\n    ),\n  },\n});\n```\n\n### 3. Render AI-Generated Specs\n\n```tsx\nfunction Dashboard({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n**That's it.** AI generates JSON, you render it safely.\n\n---\n\n## Packages\n\n| Package                     | Description                                                            |\n| --------------------------- | ---------------------------------------------------------------------- |\n| `@json-render/core`         | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities     |\n| `@json-render/react`        | React renderer, contexts, hooks                                        |\n| `@json-render/vue`          | Vue 3 renderer, composables, providers                                 |\n| `@json-render/svelte`       | Svelte 5 renderer with runes-based reactivity                          |\n| `@json-render/solid`        | SolidJS renderer with fine-grained reactive contexts                   |\n| `@json-render/shadcn`       | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS)            |\n| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components)  |\n| `@json-render/react-native` | React Native renderer with standard mobile components                  |\n| `@json-render/remotion`     | Remotion video renderer, timeline schema                               |\n| `@json-render/react-pdf`    | React PDF renderer for generating PDF documents from specs             |\n| `@json-render/react-email`  | React Email renderer for HTML/plain-text emails from specs             |\n| `@json-render/image`        | Image renderer for SVG/PNG output (OG images, social cards) via Satori |\n| `@json-render/codegen`      | Utilities for generating code from json-render UI trees                |\n| `@json-render/redux`        | Redux / Redux Toolkit adapter for `StateStore`                         |\n| `@json-render/zustand`      | Zustand adapter for `StateStore`                                       |\n| `@json-render/jotai`        | Jotai adapter for `StateStore`                                         |\n| `@json-render/xstate`       | XState Store (atom) adapter for `StateStore`                           |\n| `@json-render/mcp`          | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code              |\n| `@json-render/yaml`         | YAML wire format with streaming parser, edit modes, AI SDK transform   |\n\n## Renderers\n\n### React (UI)\n\n```tsx\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { schema } from \"@json-render/react/schema\";\n\n// Flat spec format (root key + elements map)\nconst spec = {\n  root: \"card-1\",\n  elements: {\n    \"card-1\": {\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [\"button-1\"],\n    },\n    \"button-1\": {\n      type: \"Button\",\n      props: { label: \"Click me\" },\n      children: [],\n    },\n  },\n};\n\n// defineRegistry creates a type-safe component registry\nconst { registry } = defineRegistry(catalog, { components });\n<Renderer spec={spec} registry={registry} />;\n```\n\n### Vue (UI)\n\n```typescript\nimport { h } from \"vue\";\nimport { defineRegistry, Renderer } from \"@json-render/vue\";\nimport { schema } from \"@json-render/vue/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [h(\"h3\", null, props.title), children]),\n    Button: ({ props, emit }) =>\n      h(\"button\", { onClick: () => emit(\"press\") }, props.label),\n  },\n});\n\n// In your Vue component template:\n// <Renderer :spec=\"spec\" :registry=\"registry\" />\n```\n\n### Svelte (UI)\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/svelte\";\nimport { schema } from \"@json-render/svelte/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => /* Svelte 5 snippet */,\n    Button: ({ props, emit }) => /* Svelte 5 snippet */,\n  },\n});\n\n// In your Svelte component:\n// <Renderer spec={spec} registry={registry} />\n```\n\n### Solid (UI)\n\n```tsx\nimport { defineRegistry, Renderer } from \"@json-render/solid\";\nimport { schema } from \"@json-render/solid/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: (renderProps) => <div>{renderProps.children}</div>,\n    Button: (renderProps) => (\n      <button onClick={() => renderProps.emit(\"press\")}>\n        {renderProps.element.props.label as string}\n      </button>\n    ),\n  },\n});\n\n<Renderer spec={spec} registry={registry} />;\n```\n\n### shadcn/ui (Web)\n\n```tsx\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\n// Pick components from the 36 standard definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Heading: shadcnComponentDefinitions.Heading,\n    Button: shadcnComponentDefinitions.Button,\n  },\n  actions: {},\n});\n\n// Use matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Heading: shadcnComponents.Heading,\n    Button: shadcnComponents.Button,\n  },\n});\n\n<Renderer spec={spec} registry={registry} />;\n```\n\n### React Native (Mobile)\n\n```tsx\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 } from \"@json-render/react-native\";\n\n// 25+ standard components included\nconst catalog = defineCatalog(schema, {\n  components: { ...standardComponentDefinitions },\n  actions: standardActionDefinitions,\n});\n\nconst { registry } = defineRegistry(catalog, { components: {} });\n<Renderer spec={spec} registry={registry} />;\n```\n\n### Remotion (Video)\n\n```tsx\nimport { Player } from \"@remotion/player\";\nimport {\n  Renderer,\n  schema,\n  standardComponentDefinitions,\n} from \"@json-render/remotion\";\n\n// Timeline spec format\nconst spec = {\n  composition: {\n    id: \"video\",\n    fps: 30,\n    width: 1920,\n    height: 1080,\n    durationInFrames: 300,\n  },\n  tracks: [{ id: \"main\", name: \"Main\", type: \"video\", enabled: true }],\n  clips: [\n    {\n      id: \"clip-1\",\n      trackId: \"main\",\n      component: \"TitleCard\",\n      props: { title: \"Hello\" },\n      from: 0,\n      durationInFrames: 90,\n    },\n  ],\n  audio: { tracks: [] },\n};\n\n<Player\n  component={Renderer}\n  inputProps={{ spec }}\n  durationInFrames={spec.composition.durationInFrames}\n  fps={spec.composition.fps}\n  compositionWidth={spec.composition.width}\n  compositionHeight={spec.composition.height}\n/>;\n```\n\n### React PDF (Documents)\n\n```typescript\nimport { renderToBuffer } from \"@json-render/react-pdf\";\n\nconst spec = {\n  root: \"doc\",\n  elements: {\n    doc: {\n      type: \"Document\",\n      props: { title: \"Invoice\" },\n      children: [\"page-1\"],\n    },\n    \"page-1\": {\n      type: \"Page\",\n      props: { size: \"A4\" },\n      children: [\"heading-1\", \"table-1\"],\n    },\n    \"heading-1\": {\n      type: \"Heading\",\n      props: { text: \"Invoice #1234\", level: \"h1\" },\n      children: [],\n    },\n    \"table-1\": {\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\n// Render to buffer, stream, or file\nconst buffer = await renderToBuffer(spec);\n```\n\n### React Email (Email)\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\": {\n      type: \"Html\",\n      props: { lang: \"en\", dir: \"ltr\" },\n      children: [\"head-1\", \"body-1\"],\n    },\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: {\n        style: { maxWidth: \"600px\", margin: \"0 auto\", padding: \"20px\" },\n      },\n      children: [\"heading-1\", \"text-1\"],\n    },\n    \"heading-1\": { type: \"Heading\", props: { text: \"Welcome\" }, children: [] },\n    \"text-1\": {\n      type: \"Text\",\n      props: { text: \"Thanks for signing up.\" },\n      children: [],\n    },\n  },\n};\n\nconst html = await renderToHtml(spec);\n```\n\n### Image (SVG/PNG)\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\n\nconst 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\n// Render to PNG (requires @resvg/resvg-js)\nconst png = await renderToPng(spec, { fonts });\n\n// Or render to SVG string\nimport { renderToSvg } from \"@json-render/image/render\";\nconst svg = await renderToSvg(spec, { fonts });\n```\n\n### Three.js (3D)\n\n```tsx\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry } from \"@json-render/react\";\nimport {\n  threeComponentDefinitions,\n  threeComponents,\n  ThreeCanvas,\n} from \"@json-render/react-three-fiber\";\n\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\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<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## Features\n\n### Streaming (SpecStream)\n\nStream AI responses progressively:\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process chunks as they arrive\nconst { result, newPatches } = compiler.push(chunk);\nsetSpec(result); // Update UI with partial result\n\n// Get final result\nconst finalSpec = compiler.getResult();\n```\n\n### AI Prompt Generation\n\nGenerate system prompts from your catalog:\n\n```typescript\nconst systemPrompt = catalog.prompt();\n// Includes component descriptions, props schemas, available actions\n```\n\n### Conditional Visibility\n\n```json\n{\n  \"type\": \"Alert\",\n  \"props\": { \"message\": \"Error occurred\" },\n  \"visible\": [\n    { \"$state\": \"/form/hasError\" },\n    { \"$state\": \"/form/errorDismissed\", \"not\": true }\n  ]\n}\n```\n\n### Dynamic Props\n\nAny prop value can be data-driven using expressions:\n\n```json\n{\n  \"type\": \"Icon\",\n  \"props\": {\n    \"name\": {\n      \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n      \"$then\": \"home\",\n      \"$else\": \"home-outline\"\n    },\n    \"color\": {\n      \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n      \"$then\": \"#007AFF\",\n      \"$else\": \"#8E8E93\"\n    }\n  }\n}\n```\n\nExpression forms:\n\n- **`{ \"$state\": \"/state/key\" }`** - reads a value from the state model\n- **`{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }`** - evaluates a condition and picks a branch\n- **`{ \"$template\": \"Hello, ${/user/name}!\" }`** - interpolates state values into strings\n- **`{ \"$computed\": \"fn\", \"args\": { ... } }`** - calls a registered function with resolved args\n\n### Actions\n\nComponents can trigger actions, including the built-in `setState` action:\n\n```json\n{\n  \"type\": \"Pressable\",\n  \"props\": {\n    \"action\": \"setState\",\n    \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n  },\n  \"children\": [\"home-icon\"]\n}\n```\n\nThe `setState` action updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions.\n\n### State Watchers\n\nReact to state changes by triggering actions:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\", \"UK\"]\n  },\n  \"watch\": {\n    \"/form/country\": {\n      \"action\": \"loadCities\",\n      \"params\": { \"country\": { \"$state\": \"/form/country\" } }\n    }\n  }\n}\n```\n\n`watch` is a top-level field on elements (sibling of `type`/`props`/`children`). Watchers fire when the watched value changes, not on initial render.\n\n---\n\n## Demo\n\n```bash\ngit clone https://github.com/vercel-labs/json-render\ncd json-render\npnpm install\npnpm dev\n```\n\n- http://json-render.localhost:1355 - Docs & Playground\n- http://dashboard-demo.json-render.localhost:1355 - Example Dashboard\n- http://react-email-demo.json-render.localhost:1355 - React Email Example\n- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example\n- Chat Example: run `pnpm dev` in `examples/chat`\n- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`\n- Vue Example: run `pnpm dev` in `examples/vue`\n- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`\n- React Native example: run `npx expo start` in `examples/react-native`\n\n## How It Works\n\n```mermaid\nflowchart LR\n    A[User Prompt] --> B[AI + Catalog]\n    B --> C[JSON Spec]\n    C --> D[Renderer]\n\n    B -.- E([guardrailed])\n    C -.- F([predictable])\n    D -.- G([streamed])\n```\n\n1. **Define the guardrails** - what components, actions, and data bindings AI can use\n2. **Prompt** - describe what you want in natural language\n3. **AI generates JSON** - output is always predictable, constrained to your catalog\n4. **Render fast** - stream and render progressively as the model responds\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "apps/web/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Override the default model used for UI generation\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Vercel KV (Rate Limiting)\n# Automatically populated when you add Vercel KV to your project\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\n\n# Rate Limiting\n# RATE_LIMIT_PER_MINUTE=10\n# RATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "apps/web/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# env files (can opt-in for commiting if needed)\n.env*\n!.env.example\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env*.local\n"
  },
  {
    "path": "apps/web/CHANGELOG.md",
    "content": "# web\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/codegen@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/yaml@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/yaml@0.14.0\n  - @json-render/codegen@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/codegen@0.13.0\n  - @json-render/react@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/codegen@0.12.1\n  - @json-render/react@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/codegen@0.12.0\n  - @json-render/react@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/codegen@0.11.0\n  - @json-render/react@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n  - @json-render/codegen@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/core@0.9.1\n  - @json-render/codegen@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n  - @json-render/codegen@0.9.0\n"
  },
  {
    "path": "apps/web/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://json-render.localhost:1355](http://json-render.localhost:1355) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/a2ui/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/a2ui\")\n\n# A2UI Integration\n\nUse `@json-render/core` to support [A2UI](https://a2ui.org) natively.\n\n<div className=\"rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8\">\n  <p className=\"text-sm text-amber-700 dark:text-amber-300\">\n    <strong>Concept:</strong> This page demonstrates how json-render can support A2UI. The examples are illustrative and may require adaptation for production use.\n  </p>\n</div>\n\n## Native A2UI Support\n\n`@json-render/core` is schema-agnostic. Define a catalog that matches A2UI's format and build a renderer that understands it - no conversion layer needed.\n\n## Example A2UI Message\n\nA2UI uses an adjacency list model - a flat list of components with ID references. This makes it easy to patch individual components:\n\n```json\n{\n  \"surfaceUpdate\": {\n    \"surfaceId\": \"main\",\n    \"components\": [\n      {\n        \"id\": \"header\",\n        \"component\": {\n          \"Text\": {\n            \"text\": {\"literalString\": \"Book Your Table\"},\n            \"usageHint\": \"h1\"\n          }\n        }\n      },\n      {\n        \"id\": \"date-picker\",\n        \"component\": {\n          \"DateTimeInput\": {\n            \"label\": {\"literalString\": \"Select Date\"},\n            \"value\": {\"path\": \"/reservation/date\"},\n            \"enableDate\": true\n          }\n        }\n      },\n      {\n        \"id\": \"submit-btn\",\n        \"component\": {\n          \"Button\": {\n            \"child\": \"submit-text\",\n            \"action\": {\"name\": \"confirm_booking\"}\n          }\n        }\n      },\n      {\n        \"id\": \"submit-text\",\n        \"component\": {\n          \"Text\": {\"text\": {\"literalString\": \"Confirm Reservation\"}}\n        }\n      }\n    ]\n  }\n}\n```\n\n## Define the A2UI Catalog\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\n// A2UI BoundValue schema\nconst BoundString = z.object({\n  literalString: z.string().optional(),\n  path: z.string().optional(),\n}).refine(d => d.literalString || d.path);\n\n// A2UI children schema\nconst Children = z.object({\n  explicitList: z.array(z.string()).optional(),\n  template: z.object({\n    dataBinding: z.string(),\n    componentId: z.string(),\n  }).optional(),\n}).refine(d => d.explicitList || d.template);\n\nexport const a2uiCatalog = defineCatalog(schema, {\n  components: {\n    Text: {\n      description: 'Displays text content',\n      props: z.object({\n        text: BoundString,\n        usageHint: z.enum(['h1', 'h2', 'h3', 'body', 'caption']).optional(),\n      }),\n    },\n    Button: {\n      description: 'Interactive button',\n      props: z.object({\n        child: z.string(),\n        action: z.object({\n          name: z.string(),\n          context: z.array(z.object({\n            key: z.string(),\n            value: BoundString,\n          })).optional(),\n        }).optional(),\n      }),\n    },\n    DateTimeInput: {\n      description: 'Date/time picker',\n      props: z.object({\n        label: BoundString.optional(),\n        value: BoundString.optional(),\n        enableDate: z.boolean().optional(),\n        enableTime: z.boolean().optional(),\n      }),\n    },\n    Column: {\n      description: 'Vertical layout',\n      props: z.object({\n        children: Children,\n      }),\n    },\n    Row: {\n      description: 'Horizontal layout',\n      props: z.object({\n        children: Children,\n      }),\n    },\n    // Add more A2UI standard components...\n  },\n});\n```\n\n## Define the A2UI Schema\n\nDefine the schema for A2UI message types:\n\n```typescript\nimport { z } from 'zod';\n\n// Component instance in the adjacency list\nconst A2UIComponent = z.object({\n  id: z.string(),\n  component: z.record(z.record(z.unknown())),\n});\n\n// Surface update message\nconst SurfaceUpdate = z.object({\n  surfaceId: z.string().optional(),\n  components: z.array(A2UIComponent),\n});\n\n// State model update message\nconst StateModelUpdate = z.object({\n  surfaceId: z.string().optional(),\n  path: z.string().optional(),\n  contents: z.array(z.object({\n    key: z.string(),\n    valueString: z.string().optional(),\n    valueNumber: z.number().optional(),\n    valueBoolean: z.boolean().optional(),\n    valueMap: z.array(z.unknown()).optional(),\n  })),\n});\n\n// Begin rendering message\nconst BeginRendering = z.object({\n  surfaceId: z.string().optional(),\n  root: z.string(),\n  catalogId: z.string().optional(),\n});\n\n// Complete A2UI message schema\nexport const A2UIMessage = z.object({\n  surfaceUpdate: SurfaceUpdate.optional(),\n  dataModelUpdate: StateModelUpdate.optional(),\n  beginRendering: BeginRendering.optional(),\n  deleteSurface: z.object({ surfaceId: z.string() }).optional(),\n});\n```\n\n## Build an A2UI Renderer\n\nCreate a renderer that processes the A2UI adjacency list format:\n\n```tsx\nimport { a2uiCatalog } from './catalog';\n\n// Component registry\nconst components = {\n  Text: ({ text, usageHint }) => {\n    const Tag = usageHint?.startsWith('h') ? usageHint : 'p';\n    return <Tag>{text}</Tag>;\n  },\n  Button: ({ children, action, onAction }) => (\n    <button onClick={() => onAction?.(action)}>{children}</button>\n  ),\n  DateTimeInput: ({ label, value, onChange }) => (\n    <label>\n      {label}\n      <input type=\"date\" value={value} onChange={e => onChange?.(e.target.value)} />\n    </label>\n  ),\n  Column: ({ children }) => <div className=\"flex flex-col gap-2\">{children}</div>,\n  Row: ({ children }) => <div className=\"flex gap-2\">{children}</div>,\n};\n\n// Render A2UI surface\nexport function renderA2UI(\n  componentMap: Map<string, any>,\n  dataModel: Record<string, any>,\n  rootId: string,\n  onAction?: (action: any) => void\n) {\n  function resolveBoundValue(bound: any) {\n    if (!bound) return undefined;\n    if (bound.literalString) return bound.literalString;\n    if (bound.path) {\n      const parts = bound.path.replace(/^\\//, '').split('/');\n      let value = dataModel;\n      for (const p of parts) value = value?.[p];\n      return value;\n    }\n  }\n\n  function render(id: string): React.ReactNode {\n    const comp = componentMap.get(id);\n    if (!comp) return null;\n\n    const [type, props] = Object.entries(comp.component)[0];\n    const Component = components[type];\n    if (!Component) return null;\n\n    // Resolve props\n    const resolved: any = {};\n    for (const [key, val] of Object.entries(props as any)) {\n      if (key === 'child') {\n        resolved.children = render(val as string);\n      } else if (key === 'children' && val?.explicitList) {\n        resolved.children = val.explicitList.map(render);\n      } else if (val && typeof val === 'object' && ('literalString' in val || 'path' in val)) {\n        resolved[key] = resolveBoundValue(val);\n      } else {\n        resolved[key] = val;\n      }\n    }\n\n    return <Component key={id} {...resolved} onAction={onAction} />;\n  }\n\n  return render(rootId);\n}\n```\n\n## Usage\n\n```tsx\nconst [components] = useState(() => new Map());\nconst [dataModel, setDataModel] = useState({});\nconst [rootId, setRootId] = useState<string | null>(null);\n\n// Process A2UI messages\nfunction handleMessage(msg: any) {\n  if (msg.surfaceUpdate) {\n    for (const comp of msg.surfaceUpdate.components) {\n      components.set(comp.id, comp);\n    }\n  }\n  if (msg.dataModelUpdate) {\n    setDataModel(prev => ({ ...prev, ...msg.dataModelUpdate.contents }));\n  }\n  if (msg.beginRendering) {\n    setRootId(msg.beginRendering.root);\n  }\n}\n\n// Render\n{rootId && renderA2UI(components, dataModel, rootId, handleAction)}\n```\n\n## Next\n\nLearn about [Adaptive Cards integration](/docs/adaptive-cards) for another UI protocol.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/adaptive-cards/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/adaptive-cards\")\n\n# Adaptive Cards Integration\n\nUse json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.\n\n<div className=\"rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8\">\n  <p className=\"text-sm text-amber-700 dark:text-amber-300\">\n    <strong>Concept:</strong> This page demonstrates how json-render can support Adaptive Cards. The examples are illustrative and may require adaptation for production use.\n  </p>\n</div>\n\n## Adaptive Cards Overview\n\nAdaptive Cards is a JSON-based format for platform-agnostic UI snippets. Cards have a `body` array of elements and an optional `actions` array for interactive buttons.\n\n### Example Adaptive Card\n\n```json\n{\n  \"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",\n  \"type\": \"AdaptiveCard\",\n  \"version\": \"1.5\",\n  \"body\": [\n    {\n      \"type\": \"TextBlock\",\n      \"text\": \"Hello, Adaptive Cards!\",\n      \"size\": \"large\",\n      \"weight\": \"bolder\"\n    },\n    {\n      \"type\": \"Image\",\n      \"url\": \"https://example.com/image.png\",\n      \"altText\": \"Example image\"\n    },\n    {\n      \"type\": \"Container\",\n      \"items\": [\n        {\n          \"type\": \"TextBlock\",\n          \"text\": \"This is inside a container\",\n          \"wrap\": true\n        }\n      ]\n    },\n    {\n      \"type\": \"ColumnSet\",\n      \"columns\": [\n        {\n          \"type\": \"Column\",\n          \"width\": \"auto\",\n          \"items\": [\n            { \"type\": \"TextBlock\", \"text\": \"Column 1\" }\n          ]\n        },\n        {\n          \"type\": \"Column\",\n          \"width\": \"stretch\",\n          \"items\": [\n            { \"type\": \"TextBlock\", \"text\": \"Column 2\" }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"Input.Text\",\n      \"id\": \"userInput\",\n      \"placeholder\": \"Enter your name\",\n      \"label\": \"Name\"\n    }\n  ],\n  \"actions\": [\n    {\n      \"type\": \"Action.Submit\",\n      \"title\": \"Submit\"\n    },\n    {\n      \"type\": \"Action.OpenUrl\",\n      \"title\": \"Learn More\",\n      \"url\": \"https://adaptivecards.io\"\n    }\n  ]\n}\n```\n\n## Creating an Adaptive Cards Catalog\n\nDefine a catalog matching the Adaptive Cards element types:\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\n// Common Adaptive Cards properties\nconst Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);\nconst HorizontalAlignment = z.enum(['left', 'center', 'right']);\nconst VerticalAlignment = z.enum(['top', 'center', 'bottom']);\nconst FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);\nconst FontWeight = z.enum(['lighter', 'default', 'bolder']);\nconst ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);\nconst ImageStyle = z.enum(['default', 'person']);\n\n// Base element properties shared by most elements\nconst BaseElement = {\n  id: z.string().optional(),\n  isVisible: z.boolean().optional(),\n  separator: z.boolean().optional(),\n  spacing: Spacing.optional(),\n};\n\nexport const adaptiveCardsCatalog = defineCatalog(schema, {\n  components: {\n    // Root card\n    AdaptiveCard: {\n      description: 'Root Adaptive Card container',\n      props: z.object({\n        version: z.string(),\n        body: z.array(z.unknown()).optional(),\n        actions: z.array(z.unknown()).optional(),\n        fallbackText: z.string().optional(),\n        minHeight: z.string().optional(),\n        rtl: z.boolean().optional(),\n        verticalContentAlignment: VerticalAlignment.optional(),\n      }),\n    },\n\n    // Elements\n    TextBlock: {\n      description: 'Displays text with formatting options',\n      props: z.object({\n        ...BaseElement,\n        text: z.string(),\n        color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),\n        fontType: z.enum(['default', 'monospace']).optional(),\n        horizontalAlignment: HorizontalAlignment.optional(),\n        isSubtle: z.boolean().optional(),\n        maxLines: z.number().optional(),\n        size: FontSize.optional(),\n        weight: FontWeight.optional(),\n        wrap: z.boolean().optional(),\n      }),\n    },\n\n    Image: {\n      description: 'Displays an image',\n      props: z.object({\n        ...BaseElement,\n        url: z.string(),\n        altText: z.string().optional(),\n        backgroundColor: z.string().optional(),\n        height: z.string().optional(),\n        width: z.string().optional(),\n        horizontalAlignment: HorizontalAlignment.optional(),\n        size: ImageSize.optional(),\n        style: ImageStyle.optional(),\n      }),\n    },\n\n    Container: {\n      description: 'Groups elements together',\n      props: z.object({\n        ...BaseElement,\n        items: z.array(z.unknown()),\n        style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),\n        verticalContentAlignment: VerticalAlignment.optional(),\n        bleed: z.boolean().optional(),\n        minHeight: z.string().optional(),\n      }),\n    },\n\n    ColumnSet: {\n      description: 'Arranges columns horizontally',\n      props: z.object({\n        ...BaseElement,\n        columns: z.array(z.unknown()),\n        horizontalAlignment: HorizontalAlignment.optional(),\n        minHeight: z.string().optional(),\n      }),\n    },\n\n    Column: {\n      description: 'A column within a ColumnSet',\n      props: z.object({\n        ...BaseElement,\n        items: z.array(z.unknown()).optional(),\n        width: z.union([z.string(), z.number()]).optional(),\n        style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),\n        verticalContentAlignment: VerticalAlignment.optional(),\n      }),\n    },\n\n    FactSet: {\n      description: 'Displays a series of facts as key/value pairs',\n      props: z.object({\n        ...BaseElement,\n        facts: z.array(z.object({\n          title: z.string(),\n          value: z.string(),\n        })),\n      }),\n    },\n\n    // Inputs\n    'Input.Text': {\n      description: 'Text input field',\n      props: z.object({\n        ...BaseElement,\n        id: z.string(),\n        isMultiline: z.boolean().optional(),\n        maxLength: z.number().optional(),\n        placeholder: z.string().optional(),\n        label: z.string().optional(),\n        value: z.string().optional(),\n        style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),\n        isRequired: z.boolean().optional(),\n        errorMessage: z.string().optional(),\n      }),\n    },\n\n    'Input.Number': {\n      description: 'Number input field',\n      props: z.object({\n        ...BaseElement,\n        id: z.string(),\n        max: z.number().optional(),\n        min: z.number().optional(),\n        placeholder: z.string().optional(),\n        label: z.string().optional(),\n        value: z.number().optional(),\n        isRequired: z.boolean().optional(),\n        errorMessage: z.string().optional(),\n      }),\n    },\n\n    'Input.Toggle': {\n      description: 'Toggle/checkbox input',\n      props: z.object({\n        ...BaseElement,\n        id: z.string(),\n        title: z.string(),\n        label: z.string().optional(),\n        value: z.string().optional(),\n        valueOff: z.string().optional(),\n        valueOn: z.string().optional(),\n        isRequired: z.boolean().optional(),\n      }),\n    },\n\n    'Input.ChoiceSet': {\n      description: 'Dropdown or radio/checkbox group',\n      props: z.object({\n        ...BaseElement,\n        id: z.string(),\n        choices: z.array(z.object({\n          title: z.string(),\n          value: z.string(),\n        })),\n        isMultiSelect: z.boolean().optional(),\n        style: z.enum(['compact', 'expanded']).optional(),\n        label: z.string().optional(),\n        value: z.string().optional(),\n        placeholder: z.string().optional(),\n        isRequired: z.boolean().optional(),\n      }),\n    },\n\n    // Actions\n    'Action.OpenUrl': {\n      description: 'Opens a URL',\n      props: z.object({\n        title: z.string().optional(),\n        url: z.string(),\n        iconUrl: z.string().optional(),\n      }),\n    },\n\n    'Action.Submit': {\n      description: 'Submits input data',\n      props: z.object({\n        title: z.string().optional(),\n        data: z.unknown().optional(),\n        iconUrl: z.string().optional(),\n      }),\n    },\n\n    'Action.ShowCard': {\n      description: 'Shows a card inline',\n      props: z.object({\n        title: z.string().optional(),\n        card: z.unknown(),\n        iconUrl: z.string().optional(),\n      }),\n    },\n\n    'Action.Execute': {\n      description: 'Universal action for bots',\n      props: z.object({\n        title: z.string().optional(),\n        verb: z.string().optional(),\n        data: z.unknown().optional(),\n        iconUrl: z.string().optional(),\n      }),\n    },\n  },\n});\n```\n\n## Building an Adaptive Cards Renderer\n\nCreate a renderer that processes Adaptive Cards JSON. See the [A2UI integration](/docs/a2ui) page for a similar pattern. The key is mapping each Adaptive Card element type to a React component, resolving nested `items` and `columns` arrays recursively.\n\n## Usage Example\n\nRender an Adaptive Card and handle actions:\n\n```tsx\n'use client';\n\nimport { AdaptiveCardRenderer } from './adaptive-card-renderer';\n\nconst card = {\n  type: 'AdaptiveCard' as const,\n  version: '1.5',\n  body: [\n    {\n      type: 'TextBlock',\n      text: 'Contact Form',\n      size: 'large',\n      weight: 'bolder',\n    },\n    {\n      type: 'Input.Text',\n      id: 'name',\n      label: 'Your Name',\n      placeholder: 'Enter your name',\n    },\n    {\n      type: 'Input.Text',\n      id: 'message',\n      label: 'Message',\n      placeholder: 'Enter your message',\n      isMultiline: true,\n    },\n  ],\n  actions: [\n    {\n      type: 'Action.Submit',\n      title: 'Send',\n      data: { action: 'submitForm' },\n    },\n  ],\n};\n\nexport function ContactCard() {\n  const handleAction = (action: any, inputData: Record<string, unknown>) => {\n    console.log('Action:', action);\n    console.log('Input data:', inputData);\n    \n    // Send to your backend\n    fetch('/api/submit', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ action, data: inputData }),\n    });\n  };\n\n  return <AdaptiveCardRenderer card={card} onAction={handleAction} />;\n}\n```\n\n## Handling Action.Execute for Bots\n\nFor bot scenarios, handle `Action.Execute` with the verb and data:\n\n```typescript\ninterface ActionExecutePayload {\n  action: {\n    type: 'Action.Execute';\n    verb: string;\n    data?: unknown;\n  };\n  inputs: Record<string, unknown>;\n}\n\nasync function handleBotAction(payload: ActionExecutePayload) {\n  const response = await fetch('/api/bot/action', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      verb: payload.action.verb,\n      data: payload.action.data,\n      inputs: payload.inputs,\n    }),\n  });\n  \n  // Bot may return a new card to render\n  const result = await response.json();\n  if (result.card) {\n    return result.card; // New AdaptiveCard to render\n  }\n}\n```\n\n## Next\n\nLearn about [A2UI integration](/docs/a2ui) for another agent-driven UI protocol.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/ag-ui/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/ag-ui\")\n\n# AG-UI Integration\n\nUse json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.\n\n<div className=\"rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8\">\n  <p className=\"text-sm text-amber-700 dark:text-amber-300\">\n    <strong>Concept:</strong> This page demonstrates how json-render can support AG-UI. The examples are illustrative and may require adaptation for production use.\n  </p>\n</div>\n\n## What is AG-UI?\n\nAG-UI is an open protocol for connecting AI agents to user interfaces. It provides a standardized way for agents to render UI components, handle user input, and manage state. The protocol uses events streamed over HTTP to update the UI in real-time.\n\n## AG-UI Event Types\n\nAG-UI defines several event types for agent-UI communication:\n\n- `TEXT_MESSAGE_START` / `TEXT_MESSAGE_CONTENT` / `TEXT_MESSAGE_END` — Streaming text messages\n- `TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` — Tool/function calls\n- `STATE_SNAPSHOT` / `STATE_DELTA` — State updates\n- `CUSTOM` — Custom events for UI rendering\n\n### Example AG-UI Event Stream\n\n```json\n{\"type\": \"RUN_STARTED\", \"threadId\": \"thread-123\", \"runId\": \"run-456\"}\n{\"type\": \"TEXT_MESSAGE_START\", \"messageId\": \"msg-1\", \"role\": \"assistant\"}\n{\"type\": \"TEXT_MESSAGE_CONTENT\", \"messageId\": \"msg-1\", \"delta\": \"Here's a dashboard for you:\"}\n{\"type\": \"TEXT_MESSAGE_END\", \"messageId\": \"msg-1\"}\n{\"type\": \"TOOL_CALL_START\", \"toolCallId\": \"tc-1\", \"toolCallName\": \"render_ui\"}\n{\"type\": \"TOOL_CALL_ARGS\", \"toolCallId\": \"tc-1\", \"delta\": \"{\\\"component\\\": \\\"Dashboard\\\", \\\"props\\\": {\\\"title\\\": \\\"Sales\\\"}}\"}\n{\"type\": \"TOOL_CALL_END\", \"toolCallId\": \"tc-1\"}\n{\"type\": \"RUN_FINISHED\"}\n```\n\n## Define the AG-UI Schema\n\nDefine schemas for AG-UI event types:\n\n```typescript\nimport { z } from 'zod';\n\n// Base event schema\nconst BaseEvent = z.object({\n  type: z.string(),\n  timestamp: z.number().optional(),\n});\n\n// Text message events\nconst TextMessageStart = BaseEvent.extend({\n  type: z.literal('TEXT_MESSAGE_START'),\n  messageId: z.string(),\n  role: z.enum(['user', 'assistant']),\n});\n\nconst TextMessageContent = BaseEvent.extend({\n  type: z.literal('TEXT_MESSAGE_CONTENT'),\n  messageId: z.string(),\n  delta: z.string(),\n});\n\nconst TextMessageEnd = BaseEvent.extend({\n  type: z.literal('TEXT_MESSAGE_END'),\n  messageId: z.string(),\n});\n\n// Tool call events\nconst ToolCallStart = BaseEvent.extend({\n  type: z.literal('TOOL_CALL_START'),\n  toolCallId: z.string(),\n  toolCallName: z.string(),\n  parentMessageId: z.string().optional(),\n});\n\nconst ToolCallArgs = BaseEvent.extend({\n  type: z.literal('TOOL_CALL_ARGS'),\n  toolCallId: z.string(),\n  delta: z.string(),\n});\n\nconst ToolCallEnd = BaseEvent.extend({\n  type: z.literal('TOOL_CALL_END'),\n  toolCallId: z.string(),\n});\n\n// State events\nconst StateSnapshot = BaseEvent.extend({\n  type: z.literal('STATE_SNAPSHOT'),\n  snapshot: z.record(z.unknown()),\n});\n\nconst StateDelta = BaseEvent.extend({\n  type: z.literal('STATE_DELTA'),\n  delta: z.array(z.object({\n    op: z.enum(['add', 'remove', 'replace']),\n    path: z.string(),\n    value: z.unknown().optional(),\n  })),\n});\n\n// Custom event for UI components\nconst CustomEvent = BaseEvent.extend({\n  type: z.literal('CUSTOM'),\n  name: z.string(),\n  value: z.unknown(),\n});\n\n// Run lifecycle events\nconst RunStarted = BaseEvent.extend({\n  type: z.literal('RUN_STARTED'),\n  threadId: z.string(),\n  runId: z.string(),\n});\n\nconst RunFinished = BaseEvent.extend({\n  type: z.literal('RUN_FINISHED'),\n});\n\nconst RunError = BaseEvent.extend({\n  type: z.literal('RUN_ERROR'),\n  message: z.string(),\n  code: z.string().optional(),\n});\n\n// Union of all events\nexport const AGUIEvent = z.discriminatedUnion('type', [\n  TextMessageStart,\n  TextMessageContent,\n  TextMessageEnd,\n  ToolCallStart,\n  ToolCallArgs,\n  ToolCallEnd,\n  StateSnapshot,\n  StateDelta,\n  CustomEvent,\n  RunStarted,\n  RunFinished,\n  RunError,\n]);\n\nexport type AGUIEvent = z.infer<typeof AGUIEvent>;\n```\n\n## Define the AG-UI Catalog\n\nCreate a catalog for UI components that agents can render:\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\nexport const aguiCatalog = defineCatalog(schema, {\n  components: {\n    Container: {\n      description: 'A container for grouping elements',\n      props: z.object({\n        direction: z.enum(['row', 'column']).optional(),\n        gap: z.enum(['none', 'sm', 'md', 'lg']).optional(),\n        padding: z.enum(['none', 'sm', 'md', 'lg']).optional(),\n      }),\n    },\n    Card: {\n      description: 'A card with optional title',\n      props: z.object({\n        title: z.string().optional(),\n        description: z.string().optional(),\n      }),\n    },\n    Text: {\n      description: 'Text content',\n      props: z.object({\n        content: z.string(),\n        variant: z.enum(['body', 'heading', 'caption', 'code']).optional(),\n      }),\n    },\n    Metric: {\n      description: 'Displays a metric value',\n      props: z.object({\n        label: z.string(),\n        value: z.union([z.string(), z.number()]),\n        change: z.number().optional(),\n        format: z.enum(['number', 'currency', 'percent']).optional(),\n      }),\n    },\n    Button: {\n      description: 'Interactive button',\n      props: z.object({\n        label: z.string(),\n        variant: z.enum(['primary', 'secondary', 'outline', 'ghost']).optional(),\n        disabled: z.boolean().optional(),\n      }),\n    },\n    Alert: {\n      description: 'Alert message',\n      props: z.object({\n        message: z.string(),\n        type: z.enum(['info', 'success', 'warning', 'error']).optional(),\n      }),\n    },\n    // Add more components...\n  },\n\n  actions: {\n    submit: {\n      description: 'Submit form data',\n      params: z.object({ formId: z.string() }),\n    },\n    navigate: {\n      description: 'Navigate to a URL',\n      params: z.object({ url: z.string() }),\n    },\n    callback: {\n      description: 'Trigger a callback to the agent',\n      params: z.object({\n        name: z.string(),\n        data: z.record(z.unknown()).optional(),\n      }),\n    },\n  },\n});\n```\n\n## Build an AG-UI Event Processor\n\nProcess AG-UI events and render UI components:\n\n```tsx\n'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { AGUIEvent } from './schema';\n\ninterface AGUIState {\n  messages: Array<{\n    id: string;\n    role: 'user' | 'assistant';\n    content: string;\n  }>;\n  toolCalls: Map<string, {\n    name: string;\n    args: string;\n    result?: unknown;\n  }>;\n  state: Record<string, unknown>;\n  isRunning: boolean;\n}\n\nexport function useAGUI() {\n  const [aguiState, setAGUIState] = useState<AGUIState>({\n    messages: [],\n    toolCalls: new Map(),\n    state: {},\n    isRunning: false,\n  });\n\n  const processEvent = useCallback((event: AGUIEvent) => {\n    switch (event.type) {\n      case 'RUN_STARTED':\n        setAGUIState(prev => ({ ...prev, isRunning: true }));\n        break;\n      case 'RUN_FINISHED':\n        setAGUIState(prev => ({ ...prev, isRunning: false }));\n        break;\n      case 'TEXT_MESSAGE_START':\n        setAGUIState(prev => ({\n          ...prev,\n          messages: [...prev.messages, {\n            id: event.messageId,\n            role: event.role,\n            content: '',\n          }],\n        }));\n        break;\n      case 'TEXT_MESSAGE_CONTENT':\n        setAGUIState(prev => ({\n          ...prev,\n          messages: prev.messages.map(msg =>\n            msg.id === event.messageId\n              ? { ...msg, content: msg.content + event.delta }\n              : msg\n          ),\n        }));\n        break;\n      case 'TOOL_CALL_START':\n        setAGUIState(prev => {\n          const toolCalls = new Map(prev.toolCalls);\n          toolCalls.set(event.toolCallId, { name: event.toolCallName, args: '' });\n          return { ...prev, toolCalls };\n        });\n        break;\n      case 'TOOL_CALL_ARGS':\n        setAGUIState(prev => {\n          const toolCalls = new Map(prev.toolCalls);\n          const tc = toolCalls.get(event.toolCallId);\n          if (tc) {\n            toolCalls.set(event.toolCallId, { ...tc, args: tc.args + event.delta });\n          }\n          return { ...prev, toolCalls };\n        });\n        break;\n      case 'STATE_SNAPSHOT':\n        setAGUIState(prev => ({ ...prev, state: event.snapshot }));\n        break;\n    }\n  }, []);\n\n  return { state: aguiState, processEvent };\n}\n```\n\n## Usage Example\n\n```tsx\n'use client';\n\nimport { useAGUI } from './use-agui';\nimport { renderToolCallUI } from './renderer';\n\nexport function AGUIChat() {\n  const { state, processEvent } = useAGUI();\n\n  async function startRun(prompt: string) {\n    const response = await fetch('/api/agent', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ prompt }),\n    });\n\n    const reader = response.body?.getReader();\n    const decoder = new TextDecoder();\n\n    while (reader) {\n      const { done, value } = await reader.read();\n      if (done) break;\n\n      const lines = decoder.decode(value).split('\\n').filter(Boolean);\n      for (const line of lines) {\n        const event = JSON.parse(line);\n        processEvent(event);\n      }\n    }\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      {state.messages.map(msg => (\n        <div key={msg.id} className={`p-3 rounded ${\n          msg.role === 'assistant' ? 'bg-muted' : 'bg-primary/10'\n        }`}>\n          {msg.content}\n        </div>\n      ))}\n\n      {Array.from(state.toolCalls.values()).map((tc, i) => (\n        <div key={i}>{renderToolCallUI(tc)}</div>\n      ))}\n\n      <form onSubmit={(e) => {\n        e.preventDefault();\n        const input = e.currentTarget.querySelector('input');\n        if (input?.value) {\n          startRun(input.value);\n          input.value = '';\n        }\n      }}>\n        <input\n          type=\"text\"\n          placeholder=\"Ask the agent...\"\n          className=\"w-full px-4 py-2 border rounded\"\n          disabled={state.isRunning}\n        />\n      </form>\n    </div>\n  );\n}\n```\n\n## Next\n\nLearn about [OpenAPI integration](/docs/openapi) for rendering forms from API schemas.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/ai-sdk/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/ai-sdk\")\n\n# AI SDK Integration\n\nUse json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.\n\n## Installation\n\n```bash\nnpm install ai @ai-sdk/react\n```\n\n## Standalone Mode\n\nIn standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.\n\n### API Route\n\n```typescript\n// app/api/generate/route.ts\nimport { streamText } from \"ai\";\nimport { catalog } from \"@/lib/catalog\";\n\nexport async function POST(req: Request) {\n  const { prompt, currentTree } = await req.json();\n\n  const systemPrompt = catalog.prompt();\n\n  // Optionally include current UI state for context\n  const contextPrompt = currentTree\n    ? `\\n\\nCurrent UI state:\\n${JSON.stringify(currentTree, null, 2)}`\n    : \"\";\n\n  const result = streamText({\n    model: yourModel,\n    system: systemPrompt + contextPrompt,\n    prompt,\n  });\n\n  return result.toTextStreamResponse();\n}\n```\n\n### Client\n\nUse `useUIStream` on the client to compile the JSONL stream into a spec:\n\n```tsx\n\"use client\";\n\nimport { useUIStream, Renderer } from \"@json-render/react\";\n\nfunction GenerativeUI() {\n  const { spec, isStreaming, error, send } = useUIStream({\n    api: \"/api/generate\",\n  });\n\n  return (\n    <div>\n      <button\n        onClick={() => send(\"Create a dashboard with metrics\")}\n        disabled={isStreaming}\n      >\n        {isStreaming ? \"Generating...\" : \"Generate\"}\n      </button>\n\n      {error && <p className=\"text-red-500\">{error.message}</p>}\n\n      <Renderer spec={spec} registry={registry} loading={isStreaming} />\n    </div>\n  );\n}\n```\n\n## Inline Mode\n\nIn inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.\n\n### API Route\n\nUse `pipeJsonRender` to separate text from JSONL patches in the stream. Patches are emitted as data parts that the client can pick up.\n\n```typescript\n// app/api/chat/route.ts\nimport { streamText } from \"ai\";\nimport { pipeJsonRender } from \"@json-render/core\";\nimport {\n  createUIMessageStream,\n  createUIMessageStreamResponse,\n} from \"ai\";\nimport { catalog } from \"@/lib/catalog\";\n\nexport async function POST(req: Request) {\n  const { messages } = await req.json();\n\n  const result = streamText({\n    model: yourModel,\n    system: catalog.prompt({ mode: \"inline\" }),\n    messages,\n  });\n\n  const stream = createUIMessageStream({\n    execute: async ({ writer }) => {\n      writer.merge(pipeJsonRender(result.toUIMessageStream()));\n    },\n  });\n\n  return createUIMessageStreamResponse({ stream });\n}\n```\n\n### Client\n\nUse `useChat` from the AI SDK and `useJsonRenderMessage` from json-render to extract the spec from each message:\n\n```tsx\n\"use client\";\n\nimport { useChat } from \"@ai-sdk/react\";\nimport { useJsonRenderMessage, Renderer } from \"@json-render/react\";\n\nfunction Chat() {\n  const { messages, input, handleInputChange, handleSubmit } = useChat({\n    api: \"/api/chat\",\n  });\n\n  return (\n    <div>\n      <div>\n        {messages.map((msg) => (\n          <ChatMessage key={msg.id} message={msg} />\n        ))}\n      </div>\n\n      <form onSubmit={handleSubmit}>\n        <input\n          value={input}\n          onChange={handleInputChange}\n          placeholder=\"Ask something...\"\n        />\n        <button type=\"submit\">Send</button>\n      </form>\n    </div>\n  );\n}\n\nfunction ChatMessage({ message }: { message: { parts: Array<{ type: string; text?: string; data?: unknown }> } }) {\n  const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);\n\n  return (\n    <div>\n      {text && <p>{text}</p>}\n      {hasSpec && spec && (\n        <Renderer spec={spec} registry={registry} />\n      )}\n    </div>\n  );\n}\n```\n\n## Prompt Engineering\n\nThe `catalog.prompt()` method creates an optimized system prompt that:\n\n- Lists all available components and their props\n- Describes available actions\n- Specifies the expected output format (JSONL-only or text + JSONL depending on mode)\n- Includes examples for better generation\n\n### Custom Rules\n\nPass custom rules to tailor AI behavior:\n\n```typescript\nconst systemPrompt = catalog.prompt({\n  customRules: [\n    \"Always use Card components for grouping related content\",\n    \"Prefer horizontal layouts (Row) for metrics\",\n    \"Use consistent spacing with padding=\\\"md\\\"\",\n  ],\n});\n```\n\n### Inline Mode Prompt\n\n```typescript\nconst inlinePrompt = catalog.prompt({ mode: \"inline\" });\n```\n\nIn inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.\n\n## Which Mode?\n\n<div className=\"my-6 overflow-x-auto\">\n  <table className=\"mdx-table w-full text-sm border-collapse\">\n    <thead>\n      <tr>\n        <th></th>\n        <th>Standalone</th>\n        <th>Inline</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>Output</td>\n        <td>JSONL only</td>\n        <td>Text + JSONL</td>\n      </tr>\n      <tr>\n        <td>Text-only replies</td>\n        <td>No</td>\n        <td>Yes</td>\n      </tr>\n      <tr>\n        <td>System prompt</td>\n        <td><code>catalog.prompt()</code></td>\n        <td><code>{\"catalog.prompt({ mode: \\\"inline\\\" })\"}</code></td>\n      </tr>\n      <tr>\n        <td>Stream utility</td>\n        <td><code>useUIStream</code></td>\n        <td><code>pipeJsonRender</code> + <code>useJsonRenderMessage</code></td>\n      </tr>\n      <tr>\n        <td>Use case</td>\n        <td>Playgrounds, builders</td>\n        <td>Chatbots, copilots</td>\n      </tr>\n    </tbody>\n  </table>\n</div>\n\nLearn more in the [Generation Modes](/docs/generation-modes) guide.\n\n## Next\n\n- Learn about [progressive streaming](/docs/streaming)\n- See the [chat example](https://github.com/vercel-labs/json-render/tree/main/examples/chat) for a complete implementation\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/codegen/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/codegen\")\n\n# @json-render/codegen\n\nUtilities for generating code from UI trees.\n\n## Tree Traversal\n\n### traverseSpec\n\nWalk the UI spec depth-first.\n\n```typescript\nfunction traverseSpec(\n  spec: Spec,\n  visitor: TreeVisitor,\n  startKey?: string\n): void\n\ninterface TreeVisitor {\n  (element: UIElement, key: string, depth: number, parent: UIElement | null): void;\n}\n```\n\n### collectUsedComponents\n\nGet all unique component types used in a spec.\n\n```typescript\nfunction collectUsedComponents(spec: Spec): Set<string>\n\n// Example\nconst components = collectUsedComponents(spec);\n// Set { 'Card', 'Metric', 'Chart' }\n```\n\n### collectStatePaths\n\nGet all state paths referenced in props (statePath, bindPath, etc.).\n\n```typescript\nfunction collectStatePaths(spec: Spec): Set<string>\n\n// Example\nconst paths = collectStatePaths(spec);\n// Set { 'analytics/revenue', 'analytics/customers' }\n```\n\n### collectActions\n\nGet all action names used in the spec.\n\n```typescript\nfunction collectActions(spec: Spec): Set<string>\n\n// Example\nconst actions = collectActions(spec);\n// Set { 'submit_form', 'refresh_data' }\n```\n\n## Serialization\n\n### serializePropValue\n\nSerialize a single value to a code string.\n\n```typescript\nfunction serializePropValue(\n  value: unknown,\n  options?: SerializeOptions\n): { value: string; needsBraces: boolean }\n\n// Examples\nserializePropValue(\"hello\")\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue(42)\n// { value: '42', needsBraces: true }\n\nserializePropValue({ $state: '/user/name' })\n// { value: '{ $state: \"/user/name\" }', needsBraces: true }\n```\n\n### serializeProps\n\nSerialize a props object to a JSX attributes string.\n\n```typescript\nfunction serializeProps(\n  props: Record<string, unknown>,\n  options?: SerializeOptions\n): string\n\n// Example\nserializeProps({ title: 'Dashboard', columns: 3, disabled: true })\n// 'title=\"Dashboard\" columns={3} disabled'\n```\n\n### escapeString\n\nEscape a string for use in code.\n\n```typescript\nfunction escapeString(\n  str: string,\n  quotes?: 'single' | 'double'\n): string\n```\n\n## Types\n\n### GeneratedFile\n\n```typescript\ninterface GeneratedFile {\n  /** File path relative to project root */\n  path: string;\n  /** File contents */\n  content: string;\n}\n```\n\n### CodeGenerator\n\n```typescript\ninterface CodeGenerator {\n  /** Generate files from a UI spec */\n  generate(spec: Spec): GeneratedFile[];\n}\n```\n\n### SerializeOptions\n\n```typescript\ninterface SerializeOptions {\n  /** Quote style for strings */\n  quotes?: 'single' | 'double';\n  /** Indent for objects/arrays */\n  indent?: number;\n}\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/core/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/core\")\n\n# @json-render/core\n\nCore types, schemas, and utilities.\n\n## defineCatalog\n\nCreates a type-safe catalog definition with schema validation.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\n\nfunction defineCatalog<T extends ZodType>(\n  s: T,\n  config: CatalogConfig\n): Catalog\n\n// Use the React schema for standard UI specs\nconst catalog = defineCatalog(schema, {\n  components: {...},\n  actions: {...},\n});\n```\n\n### CatalogConfig\n\n```typescript\ninterface CatalogConfig {\n  components: Record<string, ComponentDefinition>;\n  actions?: Record<string, ActionDefinition>;\n  functions?: Record<string, FunctionDefinition>;\n}\n\ninterface ComponentDefinition {\n  props: ZodObject;         // Use .nullable() for optional props\n  slots?: string[];         // Named slots (e.g., [\"default\"])\n  description?: string;     // Help AI understand usage\n}\n\ninterface ActionDefinition {\n  params?: ZodObject;\n  description?: string;\n}\n\ninterface FunctionDefinition {\n  description?: string;\n}\n```\n\n### Catalog Instance\n\nThe returned catalog provides methods for AI prompt generation, validation, and schema export:\n\n```typescript\ninterface Catalog {\n  // Data\n  readonly data: CatalogConfig;         // The catalog configuration\n  readonly componentNames: string[];    // List of component names\n  readonly actionNames: string[];       // List of action names\n\n  // AI Prompt Generation\n  prompt(options?: PromptOptions): string;\n\n  // Validation\n  validate(spec: unknown): SpecValidationResult;\n  zodSchema(): z.ZodType;               // Get the Zod schema for specs\n\n  // Export\n  jsonSchema(): object;                 // Export as JSON Schema\n}\n\ninterface PromptOptions {\n  system?: string;           // Custom system message intro\n  customRules?: string[];    // Additional rules to append\n  mode?: \"standalone\" | \"inline\" | \"generate\" | \"chat\"; // Output mode (default: \"standalone\")\n  editModes?: EditMode[];    // Edit modes to document in prompt (default: [\"patch\"])\n}\n\ninterface SpecValidationResult<T> {\n  success: boolean;\n  data?: T;               // Validated spec (if success)\n  error?: z.ZodError;     // Validation errors (if failed)\n}\n```\n\n### Catalog Methods\n\n```typescript\n// Generate AI system prompt\nconst systemPrompt = catalog.prompt({\n  customRules: [\"Always use Card as root element\"],\n});\n\n// Validate a spec from AI\nconst result = catalog.validate(aiOutput);\nif (result.success) {\n  render(result.data);\n} else {\n  console.error(result.error);\n}\n\n// Get Zod schema for custom validation\nconst schema = catalog.zodSchema();\nconst parsed = schema.safeParse(aiOutput);\n\n// Export as JSON Schema (for structured outputs)\nconst jsonSchema = catalog.jsonSchema();\n```\n\n## Schema System\n\njson-render uses a flexible schema system that defines both the AI output format (spec) and what catalogs must provide. Each renderer package provides its own schema (e.g., @json-render/react exports `schema`).\n\n### schema\n\nThe schema for flat UI element trees. This is exported from @json-render/react.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\n\n// schema defines:\n// - Spec shape: { root: string, elements: Record<string, UIElement> }\n// - Catalog shape: { components: {...}, actions: {...} }\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n      description: \"Container card\",\n    },\n  },\n  actions: {\n    submit: {\n      params: z.object({ formId: z.string() }),\n      description: \"Submit a form\",\n    },\n  },\n});\n```\n\n### SchemaOptions\n\nWhen creating schemas with `defineSchema`, you can pass options:\n\n```typescript\ninterface SchemaOptions {\n  promptTemplate?: PromptTemplate;  // Custom AI prompt generator\n  defaultRules?: string[];          // Default rules injected before custom rules in prompts\n  builtInActions?: BuiltInAction[]; // Actions always available at runtime, auto-injected into prompts\n}\n\ninterface BuiltInAction {\n  name: string;        // Action name (e.g. \"setState\")\n  description: string; // Human-readable description for the LLM\n}\n```\n\nBuilt-in actions are injected into prompts as `[built-in]` and are handled by the runtime (e.g. `ActionProvider`) without requiring handlers in `defineRegistry`. The React schema declares `setState`, `pushState`, and `removeState` as built-in.\n\n### defineSchema\n\nCreate custom schemas for different output formats (e.g., page-based, block-based).\n\n```typescript\nimport { defineSchema } from '@json-render/core';\n\nconst mySchema = defineSchema((s) => ({\n  // What the AI outputs (spec)\n  spec: s.object({\n    title: s.string(),\n    blocks: s.array(s.object({\n      type: s.ref(\"catalog.blocks\"),\n      content: s.any(),\n    })),\n  }),\n\n  // What the catalog must provide\n  catalog: s.object({\n    blocks: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n  }),\n}));\n```\n\n### Schema Builder API\n\nThe schema builder provides these methods:\n\n```typescript\n// Primitive types\ns.string()           // String value\ns.number()           // Number value\ns.boolean()          // Boolean value\ns.any()              // Any value\n\n// Compound types\ns.array(item)        // Array of items\ns.object({ ... })    // Object with shape\ns.record(value)      // Record/map with value type\n\n// Catalog references (for type safety)\ns.ref(\"catalog.components\")      // Reference to catalog key (becomes enum)\ns.propsOf(\"catalog.components\")  // Props schema from catalog entry\n\n// Catalog definitions\ns.map({ props: s.zod(), ... })   // Map of named entries with shared shape\ns.zod()                          // Placeholder for user-provided Zod schema\n\n// Modifiers\ns.optional()         // Mark field as optional\n```\n\n## Zod Schemas\n\nPre-built Zod schemas for common json-render types:\n\n### Dynamic Value Schemas\n\n```typescript\nimport {\n  DynamicValueSchema,    // string | number | boolean | null | { $state: string }\n  DynamicStringSchema,   // string | { $state: string }\n  DynamicNumberSchema,   // number | { $state: string }\n  DynamicBooleanSchema,  // boolean | { $state: string }\n} from '@json-render/core';\n\n// Dynamic values can be literals or state path references\ntype DynamicValue<T> = T | { $state: string };\n\n// Example: a prop that can be a literal or bound to state\nconst schema = z.object({\n  label: DynamicStringSchema,  // \"Hello\" or { $state: \"/user/name\" }\n});\n```\n\n### Visibility Schemas\n\n```typescript\nimport { VisibilityConditionSchema } from '@json-render/core';\n\n// Use in component props that need conditional rendering\nconst schema = z.object({\n  visible: VisibilityConditionSchema.optional(),\n});\n```\n\n### Action Schemas\n\n```typescript\nimport {\n  ActionSchema,           // Full action definition\n  ActionConfirmSchema,    // Confirmation dialog config\n  ActionOnSuccessSchema,  // Success handler config\n  ActionOnErrorSchema,    // Error handler config\n} from '@json-render/core';\n```\n\n### Validation Schemas\n\n```typescript\nimport {\n  ValidationCheckSchema,   // Single validation check\n  ValidationConfigSchema,  // Full validation config with checks array\n} from '@json-render/core';\n```\n\n## SpecStream\n\nSpecStream is json-render's streaming format for progressively building specs from JSONL patches.\n\n### createSpecStreamCompiler\n\nCreate a streaming compiler that incrementally builds a spec:\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 spec = compiler.getResult();\n\n// Reset for reuse\ncompiler.reset();\n```\n\n### compileSpecStream\n\nCompile an entire SpecStream string at once:\n\n```typescript\nimport { compileSpecStream } from '@json-render/core';\n\nconst jsonl = `{\"op\":\"add\",\"path\":\"/root\",\"value\":{}}\n{\"op\":\"add\",\"path\":\"/root/type\",\"value\":\"Card\"}`;\n\nconst spec = compileSpecStream<MySpec>(jsonl);\n```\n\n### Low-Level Utilities\n\n```typescript\nimport {\n  parseSpecStreamLine,\n  applySpecStreamPatch,\n} from '@json-render/core';\n\n// Parse a single line\nconst patch = parseSpecStreamLine('{\"op\":\"add\",\"path\":\"/root\",\"value\":{}}');\n\n// Apply patch to object (mutates in place)\nconst obj = {};\napplySpecStreamPatch(obj, patch);\n```\n\n### applySpecPatch\n\nApply a single SpecStream patch to a Spec object (mutates in place, returns the spec):\n\n```typescript\nimport { applySpecPatch } from '@json-render/core';\n\nlet spec: Spec = { root: \"\", elements: {} };\napplySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n\n// For React state updates, spread to create a new reference:\nsetSpec({ ...applySpecPatch(spec, patch) });\n```\n\n### nestedToFlat\n\nConvert a nested element tree (with inline children) into the flat `Spec` format:\n\n```typescript\nimport { nestedToFlat } from '@json-render/core';\n\nconst flat = nestedToFlat({\n  type: \"Card\",\n  props: { title: \"Hello\" },\n  children: [\n    { type: \"Text\", props: { content: \"World\" }, children: [] }\n  ],\n});\n// { root: \"el-0\", elements: { \"el-0\": ..., \"el-1\": ... } }\n```\n\n### createJsonRenderTransform\n\nLow-level `TransformStream` that separates text from JSONL patches in a mixed AI stream. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text.\n\nThe transform properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs, ensuring the AI SDK creates separate text parts and preserving correct interleaving of prose and UI in `message.parts`.\n\n```typescript\nimport { createJsonRenderTransform } from '@json-render/core';\n\nconst transform = createJsonRenderTransform();\n// Use with ReadableStream.pipeThrough(transform) for custom pipelines\n```\n\nMost users should use `pipeJsonRender()` instead, which wraps this transform for the common AI SDK use case.\n\n### createMixedStreamParser\n\nParse a mixed stream of text and JSONL patches (used for Inline mode):\n\n```typescript\nimport { createMixedStreamParser } from '@json-render/core';\n\nconst parser = createMixedStreamParser({\n  onText: (text) => appendToMessage(text),\n  onPatch: (patch) => applySpecPatch(spec, patch),\n});\n\n// As chunks arrive from the stream:\nfor await (const chunk of stream) {\n  parser.push(chunk);\n}\nparser.flush();\n```\n\n### pipeJsonRender\n\nPipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.\n\n```typescript\nimport { pipeJsonRender } from '@json-render/core';\nimport { createUIMessageStream, createUIMessageStreamResponse } from 'ai';\n\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeJsonRender(result.toUIMessageStream()));\n  },\n});\nreturn createUIMessageStreamResponse({ stream });\n```\n\nSee [Generation Modes](/docs/generation-modes) for full Inline mode setup.\n\n### SpecStream Types\n\nFully compliant with [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902):\n\n```typescript\ninterface SpecStreamLine {\n  op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';\n  path: string;\n  value?: unknown;  // Required for add, replace, test\n  from?: string;    // Required for move, copy\n}\n\ninterface SpecStreamCompiler<T> {\n  push(chunk: string): { result: T; newPatches: SpecStreamLine[] };\n  getResult(): T;\n  getPatches(): SpecStreamLine[];\n  reset(): void;\n}\n\ninterface MixedStreamCallbacks {\n  onText: (text: string) => void;\n  onPatch: (patch: SpecStreamLine) => void;\n}\n\ninterface MixedStreamParser {\n  push(chunk: string): void;\n  flush(): void;\n}\n```\n\n## Utility Functions\n\n### Path Utilities\n\n```typescript\nimport { getByPath, setByPath } from '@json-render/core';\n\n// Get value by JSON Pointer path\nconst value = getByPath(state, '/user/name');  // \"Alice\"\n\n// Set value by path (mutates object)\nsetByPath(state, '/user/email', 'alice@example.com');\n```\n\n### resolveDynamicValue\n\n```typescript\nimport { resolveDynamicValue } from '@json-render/core';\n\n// Resolve a dynamic value against state\nconst name = resolveDynamicValue(\"Hello\", state);        // \"Hello\"\nconst name2 = resolveDynamicValue({ $state: \"/user/name\" }, state);  // \"Alice\"\n```\n\n### findFormValue\n\n```typescript\nimport { findFormValue } from '@json-render/core';\n\n// Find form values regardless of path format\n// Checks: params.name, params[\"form.name\"], state[\"form.name\"], state.form.name\nconst value = findFormValue(\"name\", params, state);\n```\n\n## buildUserPrompt\n\nBuild structured user prompts for AI generation, with support for refinement and state context.\n\n```typescript\nimport { buildUserPrompt } from '@json-render/core';\n\nfunction buildUserPrompt(options: UserPromptOptions): string\n\ninterface UserPromptOptions {\n  prompt: string;                        // The user's text prompt\n  currentSpec?: Spec | null;             // Existing spec to refine (triggers edit mode)\n  state?: Record<string, unknown> | null; // Runtime state context to include\n  maxPromptLength?: number;              // Max length for user text (truncates before wrapping)\n  editModes?: EditMode[];                // Edit modes for refinement (default: [\"patch\"])\n}\n```\n\n### Fresh generation\n\n```typescript\nconst userPrompt = buildUserPrompt({ prompt: \"create a todo app\" });\n```\n\n### Refinement (edit modes)\n\nWhen `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes instead of recreating the entire spec. Available modes: `\"patch\"` (RFC 6902), `\"merge\"` (RFC 7396), and `\"diff\"` (unified diff).\n\n```typescript\nconst userPrompt = buildUserPrompt({\n  prompt: \"add a dark mode toggle\",\n  currentSpec: existingSpec,\n  editModes: [\"patch\", \"merge\"],\n});\n```\n\n### With state context\n\nInclude runtime state so the AI knows what data is available:\n\n```typescript\nconst userPrompt = buildUserPrompt({\n  prompt: \"show my data\",\n  state: { todos: [{ text: \"Buy milk\" }] },\n});\n```\n\n## Edit Modes\n\nUniversal edit mode utilities for modifying existing specs. Used by `buildUserPrompt` internally and available for direct use.\n\n```typescript\nimport {\n  buildEditInstructions,\n  buildEditUserPrompt,\n  isNonEmptySpec,\n  type EditMode,\n  type EditConfig,\n} from '@json-render/core';\n\ntype EditMode = \"patch\" | \"merge\" | \"diff\";\n```\n\n### buildEditInstructions\n\nGenerate the prompt section describing available edit modes. Supports both JSON and YAML formats.\n\n```typescript\nfunction buildEditInstructions(config: EditConfig, format: \"json\" | \"yaml\"): string\n\nconst instructions = buildEditInstructions({ modes: [\"patch\", \"merge\"] }, \"json\");\n```\n\n### buildEditUserPrompt\n\nBuild a user prompt for editing an existing spec. Includes the current spec (with line numbers when diff mode is enabled) and mode-specific instructions.\n\n```typescript\nfunction buildEditUserPrompt(options: BuildEditUserPromptOptions): string\n\ninterface BuildEditUserPromptOptions {\n  prompt: string;\n  currentSpec?: Spec | null;\n  config?: EditConfig;\n  format: \"json\" | \"yaml\";\n  maxPromptLength?: number;\n  serializer?: (spec: Spec) => string;\n}\n```\n\n### isNonEmptySpec\n\nCheck whether a value is a non-empty spec (has a root string and at least one element).\n\n```typescript\nfunction isNonEmptySpec(spec: unknown): spec is Spec\n```\n\n## Deep Merge and Diff\n\nFormat-agnostic utilities for merging and diffing spec objects.\n\n### deepMergeSpec\n\nDeep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse. Neither input is mutated.\n\n```typescript\nimport { deepMergeSpec } from '@json-render/core';\n\nfunction deepMergeSpec(\n  base: Record<string, unknown>,\n  patch: Record<string, unknown>\n): Record<string, unknown>\n\nconst merged = deepMergeSpec(currentSpec, { elements: { main: { props: { title: \"New\" } } } });\n```\n\n### diffToPatches\n\nGenerate RFC 6902 JSON Patch operations that transform one object into another. Arrays are compared shallowly and replaced atomically; plain objects recurse.\n\n```typescript\nimport { diffToPatches } from '@json-render/core';\n\nfunction diffToPatches(\n  oldObj: Record<string, unknown>,\n  newObj: Record<string, unknown>,\n  basePath?: string\n): JsonPatch[]\n\nconst patches = diffToPatches(oldSpec, newSpec);\n// [{ op: \"replace\", path: \"/elements/main/props/title\", value: \"New Title\" }]\n```\n\n## evaluateVisibility\n\nEvaluates a visibility condition against the state model.\n\n```typescript\nfunction evaluateVisibility(\n  condition: VisibilityCondition | undefined,\n  ctx: VisibilityContext\n): boolean\n\ninterface VisibilityContext {\n  stateModel: StateModel;\n  repeatItem?: unknown;    // Current repeat item (inside repeat scope)\n  repeatIndex?: number;    // Current repeat array index (inside repeat scope)\n}\n\ntype VisibilityCondition =\n  | { $state: string }                                    // truthiness\n  | { $state: string; not: true }                         // falsy\n  | { $state: string; eq: unknown }                       // equality\n  | { $state: string; neq: unknown }                      // inequality\n  | { $state: string; gt: number }                        // greater than\n  | { $state: string; gte: number }                       // gte\n  | { $state: string; lt: number }                        // lt\n  | { $state: string; lte: number }                       // lte\n  | { $item: string }                                     // item field (repeat scope)\n  | { $item: string; eq: unknown }                        // item field equality\n  | { $index: true }                                      // index truthiness (repeat scope)\n  | { $index: true; gt: number }                          // index comparison\n  | VisibilityCondition[]                                 // implicit AND\n  | { $and: VisibilityCondition[] }                       // explicit AND\n  | { $or: VisibilityCondition[] }                        // OR\n  | boolean;                                              // always / never\n```\n\n## Types\n\n### UIElement\n\n```typescript\ninterface UIElement {\n  type: string;\n  props: Record<string, unknown>;\n  children?: string[];          // Keys of child elements\n  visible?: VisibilityCondition;\n  on?: Record<string, ActionBinding | ActionBinding[]>;  // Event bindings\n  repeat?: { statePath: string; key?: string };           // Repeat for arrays\n}\n```\n\nElements are stored in the `elements` map keyed by string IDs. The key comes from the map, not from the element itself.\n\n### Spec (Element Tree)\n\n```typescript\ninterface Spec {\n  root: string | null;                        // Key of root element\n  elements: Record<string, UIElement>;        // Flat element map\n  state?: Record<string, unknown>;            // Initial state model\n}\n```\n\nElements are stored as a flat map with string keys. The tree structure is built by following the `children` arrays.\n\n### ActionBinding\n\n```typescript\ninterface ActionBinding {\n  action: string;\n  params?: Record<string, DynamicValue>;\n  confirm?: {\n    title: string;\n    message: string;\n    variant?: 'default' | 'danger';\n  };\n  onSuccess?: { set: Record<string, unknown> };\n  onError?: { set: Record<string, unknown> };\n  preventDefault?: boolean;  // Prevent default browser behavior (e.g. navigation on links)\n}\n```\n\n### ValidationSchema\n\n```typescript\ninterface ValidationSchema {\n  checks: ValidationCheck[];\n  validateOn?: 'change' | 'blur' | 'submit';\n}\n\ninterface ValidationCheck {\n  type: string;\n  args?: Record<string, unknown>;\n  message: string;\n}\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/image/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/image\")\n\n# @json-render/image\n\nImage renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori).\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/image\n```\n\nFor PNG output, also install the optional peer dependency:\n\n```bash\nnpm install @resvg/resvg-js\n```\n\nSee the [Image example](https://github.com/vercel-labs/json-render/tree/main/examples/image) for a full working example.\n\n## schema\n\nThe image element schema for image specs. Use with `defineCatalog` from core.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema, standardComponentDefinitions } from '@json-render/image';\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Render Functions\n\nServer-side functions for producing image output. Both accept a spec and optional `RenderOptions`.\n\n```typescript\nimport { renderToSvg, renderToPng } from '@json-render/image/render';\n\nconst svg = await renderToSvg(spec, { fonts });\n\nconst png = await renderToPng(spec, { fonts });\nawait writeFile('output.png', png);\n```\n\n### RenderOptions\n\n```typescript\ninterface RenderOptions {\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;  // default: true\n  state?: Record<string, unknown>;\n  fonts?: SatoriOptions['fonts'];\n  width?: number;\n  height?: number;\n}\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Default</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>fonts</code></td>\n      <td><code>{\"SatoriOptions['fonts']\"}</code></td>\n      <td><code>[]</code></td>\n      <td>Font data for text rendering (required for meaningful output)</td>\n    </tr>\n    <tr>\n      <td><code>width</code></td>\n      <td><code>number</code></td>\n      <td>Frame prop</td>\n      <td>Override the output image width</td>\n    </tr>\n    <tr>\n      <td><code>height</code></td>\n      <td><code>number</code></td>\n      <td>Frame prop</td>\n      <td>Override the output image height</td>\n    </tr>\n    <tr>\n      <td><code>registry</code></td>\n      <td><code>{\"Record<string, ComponentRenderer>\"}</code></td>\n      <td><code>{\"{}\"}</code></td>\n      <td>Custom component map (merged with standard components)</td>\n    </tr>\n    <tr>\n      <td><code>includeStandard</code></td>\n      <td><code>boolean</code></td>\n      <td><code>true</code></td>\n      <td>Include built-in standard components</td>\n    </tr>\n    <tr>\n      <td><code>state</code></td>\n      <td><code>{\"Record<string, unknown>\"}</code></td>\n      <td><code>{\"{}\"}</code></td>\n      <td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>\n    </tr>\n  </tbody>\n</table>\n\n## Standard Components\n\n### Root\n\n#### Frame\n\nRoot image container. Defines the output image dimensions and background. Must be the root element.\n\n```typescript\n{\n  width: number;\n  height: number;\n  backgroundColor: string | null;\n  padding: number | null;\n  display: \"flex\" | \"none\" | null;\n  flexDirection: \"row\" | \"column\" | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n}\n```\n\n### Layout\n\n#### Box\n\nGeneric container with padding, margin, background, border, and flex alignment. Supports absolute positioning.\n\n```typescript\n{\n  padding: number | null;\n  paddingTop: number | null;\n  paddingBottom: number | null;\n  paddingLeft: number | null;\n  paddingRight: number | null;\n  margin: number | null;\n  backgroundColor: string | null;\n  borderWidth: number | null;\n  borderColor: string | null;\n  borderRadius: number | null;\n  flex: number | null;\n  width: number | string | null;\n  height: number | string | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n  flexDirection: \"row\" | \"column\" | null;\n  position: \"relative\" | \"absolute\" | null;\n  top: number | null;\n  left: number | null;\n  right: number | null;\n  bottom: number | null;\n  overflow: \"visible\" | \"hidden\" | null;\n}\n```\n\n#### Row\n\nHorizontal flex layout with optional wrapping.\n\n```typescript\n{\n  gap: number | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n  padding: number | null;\n  flex: number | null;\n  wrap: boolean | null;\n}\n```\n\n#### Column\n\nVertical flex layout.\n\n```typescript\n{\n  gap: number | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n  padding: number | null;\n  flex: number | null;\n}\n```\n\n### Content\n\n#### Heading\n\nHeading text at various levels. h1 is largest, h4 is smallest.\n\n```typescript\n{\n  text: string;\n  level: \"h1\" | \"h2\" | \"h3\" | \"h4\" | null;\n  color: string | null;\n  align: \"left\" | \"center\" | \"right\" | null;\n  letterSpacing: number | string | null;\n  lineHeight: number | null;\n}\n```\n\n#### Text\n\nBody text with configurable size, color, weight, and alignment.\n\n```typescript\n{\n  text: string;\n  fontSize: number | null;\n  color: string | null;\n  align: \"left\" | \"center\" | \"right\" | null;\n  fontWeight: \"normal\" | \"bold\" | null;\n  fontStyle: \"normal\" | \"italic\" | null;\n  lineHeight: number | null;\n  letterSpacing: number | string | null;\n  textDecoration: \"none\" | \"underline\" | \"line-through\" | null;\n}\n```\n\n#### Image\n\nImage from a URL with optional dimensions and fit.\n\n```typescript\n{\n  src: string;\n  width: number | null;\n  height: number | null;\n  borderRadius: number | null;\n  objectFit: \"contain\" | \"cover\" | \"fill\" | \"none\" | null;\n}\n```\n\n### Decorative\n\n#### Divider\n\nHorizontal line separator.\n\n```typescript\n{\n  color: string | null;\n  thickness: number | null;\n  marginTop: number | null;\n  marginBottom: number | null;\n}\n```\n\n#### Spacer\n\nEmpty vertical space.\n\n```typescript\n{\n  height: number | null;\n}\n```\n\n## Catalog Definitions\n\nPre-built definitions for creating image catalogs:\n\n```typescript\nimport { standardComponentDefinitions } from '@json-render/image/catalog';\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/image';\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    // Add custom components\n  },\n});\n```\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React or Satori:\n\n```typescript\nimport { schema, standardComponentDefinitions } from '@json-render/image/server';\n```\n\n## Sub-path Exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>@json-render/image</code></td>\n      <td>Full package: schema, renderer, components, render functions</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/image/server</code></td>\n      <td>Schema and catalog definitions only (no React or Satori)</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/image/catalog</code></td>\n      <td>Standard component definitions and types</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/image/render</code></td>\n      <td>Server-side render functions only</td>\n    </tr>\n  </tbody>\n</table>\n\n## Types\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>ImageSchema</code></td>\n      <td>Schema type for image specs</td>\n    </tr>\n    <tr>\n      <td><code>ImageSpec</code></td>\n      <td>Spec type for image output</td>\n    </tr>\n    <tr>\n      <td><code>RenderOptions</code></td>\n      <td>Options for render functions</td>\n    </tr>\n    <tr>\n      <td><code>ComponentRenderProps</code></td>\n      <td>Props passed to component render functions</td>\n    </tr>\n    <tr>\n      <td><code>ComponentRenderer</code></td>\n      <td>Component render function type</td>\n    </tr>\n    <tr>\n      <td><code>ComponentRegistry</code></td>\n      <td>Map of component names to render functions</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentDefinitions</code></td>\n      <td>Type of the standard component definitions object</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentProps{'<K>'}</code></td>\n      <td>Inferred props type for a standard component by name</td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/jotai/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/jotai\")\n\n# @json-render/jotai\n\nJotai adapter for json-render's `StateStore` interface.\n\n## Installation\n\n```bash\nnpm install @json-render/jotai @json-render/core @json-render/react jotai\n```\n\n## jotaiStateStore\n\nCreate a `StateStore` backed by a Jotai atom.\n\n```typescript\nimport { jotaiStateStore } from \"@json-render/jotai\";\n```\n\n### Options\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Required</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>atom</code></td>\n      <td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>\n      <td>Yes</td>\n      <td>A writable atom holding the state model.</td>\n    </tr>\n    <tr>\n      <td><code>store</code></td>\n      <td>Jotai <code>Store</code></td>\n      <td>No</td>\n      <td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Example\n\n```typescript\nimport { atom } from \"jotai\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst uiAtom = atom<Record<string, unknown>>({ count: 0 });\nconst store = jotaiStateStore({ atom: uiAtom });\n```\n\n```tsx\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Jotai */}\n</StateProvider>\n```\n\n### Shared Jotai Store\n\nIf 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```typescript\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 });\nconst store = jotaiStateStore({ atom: uiAtom, store: jStore });\n```\n\n```tsx\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## Re-exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Source</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>StateStore</code></td>\n      <td><code>@json-render/core</code></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/mcp/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/mcp\")\n\n# @json-render/mcp\n\nMCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Install\n\n```bash\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n```\n\nFor the iframe-side React UI, also install:\n\n```bash\nnpm install @json-render/react react react-dom\n```\n\nSee the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.\n\n## Overview\n\nMCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:\n\n1. Your **catalog** defines which components and actions the AI can use\n2. The **MCP server** exposes the catalog as a tool with the spec schema\n3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe\n4. The AI generates a spec, the host renders it, and users interact with the live UI\n\n## Server API\n\n### createMcpApp\n\nCreate a fully-configured MCP server. This is the main entry point.\n\n```typescript\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst server = createMcpApp({\n  name: \"My Dashboard\",\n  version: \"1.0.0\",\n  catalog: myCatalog,\n  html: fs.readFileSync(\"dist/index.html\", \"utf-8\"),\n});\n\nawait server.connect(new StdioServerTransport());\n```\n\n#### CreateMcpAppOptions\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>name</code></td>\n      <td><code>string</code></td>\n      <td>Server name shown in client UIs</td>\n    </tr>\n    <tr>\n      <td><code>version</code></td>\n      <td><code>string</code></td>\n      <td>Server version</td>\n    </tr>\n    <tr>\n      <td><code>catalog</code></td>\n      <td><code>Catalog</code></td>\n      <td>json-render catalog defining available components</td>\n    </tr>\n    <tr>\n      <td><code>html</code></td>\n      <td><code>string</code></td>\n      <td>Self-contained HTML for the iframe UI</td>\n    </tr>\n    <tr>\n      <td><code>tool</code></td>\n      <td><code>McpToolOptions</code></td>\n      <td>Optional tool name/title/description overrides</td>\n    </tr>\n  </tbody>\n</table>\n\n### registerJsonRenderTool\n\nRegister a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.\n\n```typescript\nimport { registerJsonRenderTool } from \"@json-render/mcp\";\n\nregisterJsonRenderTool(server, {\n  catalog,\n  name: \"render-ui\",\n  title: \"Render UI\",\n  description: \"Render an interactive UI\",\n  resourceUri: \"ui://render-ui/view.html\",\n});\n```\n\n### registerJsonRenderResource\n\nRegister the UI resource that serves the bundled HTML.\n\n```typescript\nimport { registerJsonRenderResource } from \"@json-render/mcp\";\n\nregisterJsonRenderResource(server, {\n  resourceUri: \"ui://render-ui/view.html\",\n  html: bundledHtml,\n});\n```\n\n## Client API (`@json-render/mcp/app`)\n\nThese exports run inside the sandboxed iframe rendered by the MCP host.\n\n### useJsonRenderApp\n\nReact hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.\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, connected, error } = useJsonRenderApp({\n    name: \"my-app\",\n    version: \"1.0.0\",\n  });\n\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting...</div>;\n\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\n#### UseJsonRenderAppReturn\n\n<table>\n  <thead>\n    <tr>\n      <th>Field</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>spec</code></td>\n      <td><code>{'Spec | null'}</code></td>\n      <td>Current json-render spec</td>\n    </tr>\n    <tr>\n      <td><code>loading</code></td>\n      <td><code>boolean</code></td>\n      <td>Whether the spec is still being received</td>\n    </tr>\n    <tr>\n      <td><code>connected</code></td>\n      <td><code>boolean</code></td>\n      <td>Whether connected to the host</td>\n    </tr>\n    <tr>\n      <td><code>connecting</code></td>\n      <td><code>boolean</code></td>\n      <td>Whether currently connecting</td>\n    </tr>\n    <tr>\n      <td><code>error</code></td>\n      <td><code>{'Error | null'}</code></td>\n      <td>Connection error, if any</td>\n    </tr>\n    <tr>\n      <td><code>app</code></td>\n      <td><code>{'App | null'}</code></td>\n      <td>The underlying MCP App instance</td>\n    </tr>\n    <tr>\n      <td><code>callServerTool</code></td>\n      <td><code>{'(name, args?) => Promise<void>'}</code></td>\n      <td>Call an MCP server tool and update spec from result</td>\n    </tr>\n  </tbody>\n</table>\n\n### buildAppHtml\n\nGenerate a self-contained HTML page from bundled JavaScript and CSS.\n\n```typescript\nimport { buildAppHtml } from \"@json-render/mcp/app\";\nimport fs from \"node:fs\";\n\nconst html = buildAppHtml({\n  title: \"Dashboard\",\n  js: fs.readFileSync(\"dist/app.js\", \"utf-8\"),\n  css: fs.readFileSync(\"dist/app.css\", \"utf-8\"),\n});\n```\n\n## Client Configuration\n\n### Cursor\n\nAdd to `.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"json-render\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### Claude Desktop\n\nAdd to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"json-render\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/absolute/path/to/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## Supported Clients\n\nMCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/react/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/react\")\n\n# @json-render/react\n\nReact components, providers, and hooks.\n\n## Providers\n\n### StateProvider\n\n```tsx\n<StateProvider initialState={object} onStateChange={fn}>\n  {children}\n</StateProvider>\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Prop</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>store</code></td>\n      <td><code>StateStore</code></td>\n      <td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td>\n    </tr>\n    <tr>\n      <td><code>initialState</code></td>\n      <td><code>Record&lt;string, unknown&gt;</code></td>\n      <td>Initial state model (uncontrolled mode).</td>\n    </tr>\n    <tr>\n      <td><code>onStateChange</code></td>\n      <td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td>\n      <td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td>\n    </tr>\n  </tbody>\n</table>\n\n#### External Store (Controlled Mode)\n\nPass a `StateStore` to bypass the internal state and wire json-render to any state management library:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>\n  {children}\n</StateProvider>\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\nThe `store` prop is also available on `JSONUIProvider` and `createRenderer`.\n\n### ActionProvider\n\n```tsx\n<ActionProvider handlers={Record<string, ActionHandler>}>\n  {children}\n</ActionProvider>\n\ntype ActionHandler = (params: Record<string, unknown>) => void | Promise<void>;\n```\n\n### VisibilityProvider\n\n```tsx\n<VisibilityProvider>\n  {children}\n</VisibilityProvider>\n```\n\n`VisibilityProvider` reads state from the parent `StateProvider` automatically. Conditions in specs use the `VisibilityCondition` format with `$state` paths (e.g. `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`). See [visibility](/docs/visibility) for the full syntax.\n\n### ValidationProvider\n\n```tsx\n<ValidationProvider customFunctions={Record<string, ValidationFunction>}>\n  {children}\n</ValidationProvider>\n\ntype ValidationFunction = (value: unknown, args?: object) => boolean | Promise<boolean>;\n```\n\n## defineRegistry\n\nCreate a type-safe component registry from a catalog. Components receive `props`, `children`, `emit`, `on`, and `loading` with catalog-inferred types.\n\nWhen the catalog declares actions, the `actions` field is required. When the catalog has no actions (e.g. `actions: {}`), the field is optional.\n\n```tsx\nimport { defineRegistry } from '@json-render/react';\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => <div>{props.title}{children}</div>,\n    Button: ({ props, emit }) => (\n      <button onClick={() => emit(\"press\")}>\n        {props.label}\n      </button>\n    ),\n  },\n});\n\n// Pass to <Renderer>\n<Renderer spec={spec} registry={registry} />\n```\n\n## Components\n\n### Renderer\n\n```tsx\n<Renderer\n  spec={Spec}           // The UI spec to render\n  registry={Registry}   // Component registry (from defineRegistry)\n  loading={boolean}     // Optional loading state\n  fallback={Component}  // Optional fallback for unknown types\n/>\n\ntype Registry = Record<string, React.ComponentType<ComponentRenderProps>>;\n```\n\n### JSONUIProvider\n\nConvenience wrapper that combines `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`. Accepts all their props plus:\n\n<table>\n  <thead>\n    <tr>\n      <th>Prop</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>functions</code></td>\n      <td><code>Record&lt;string, ComputedFunction&gt;</code></td>\n      <td>Named functions for <code>$computed</code> expressions in props</td>\n    </tr>\n  </tbody>\n</table>\n\n```tsx\n<JSONUIProvider\n  spec={spec}\n  catalog={catalog}\n  handlers={{ submit: async () => { /* ... */ } }}\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\nThe `functions` prop is also available on `createRenderer`.\n\n### Component Props (via defineRegistry)\n\n```tsx\ninterface ComponentContext<P> {\n  props: P;                                // Typed props from catalog\n  children?: React.ReactNode;              // Rendered children (for slot components)\n  emit: (event: string) => void;           // Emit a named event (always defined)\n  on: (event: string) => EventHandle;      // Get event handle with metadata\n  loading?: boolean;\n  bindings?: Record<string, string>;       // State paths from $bindState/$bindItem expressions\n}\n\ninterface EventHandle {\n  emit: () => void;              // Fire the event\n  shouldPreventDefault: boolean; // Whether any binding requested preventDefault\n  bound: boolean;                // Whether any handler is bound\n}\n```\n\nUse `emit(\"press\")` for simple event firing. Use `on(\"click\")` when you need to check metadata like `shouldPreventDefault`:\n\n```tsx\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a\n      href={props.href}\n      onClick={(e) => {\n        if (click.shouldPreventDefault) e.preventDefault();\n        click.emit();\n      }}\n    >\n      {props.label}\n    </a>\n  );\n},\n```\n\n### BaseComponentProps\n\nCatalog-agnostic base type for building reusable component libraries (e.g. `@json-render/shadcn`) that are not tied to a specific catalog:\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## Hooks\n\n### useUIStream\n\n```typescript\nconst {\n  spec,         // Spec | null - current UI state\n  isStreaming,  // boolean - true while streaming\n  error,        // Error | null\n  send,         // (prompt: string, context?: Record<string, unknown>) => Promise<void>\n  clear,        // () => void - reset spec and error\n} = useUIStream({\n  api: string,                         // API endpoint URL\n  onComplete?: (spec: Spec) => void,   // Called when streaming completes\n  onError?: (error: Error) => void,    // Called when an error occurs\n});\n```\n\n### useStateStore\n\n```typescript\nconst {\n  state,   // StateModel (Record<string, unknown>)\n  get,     // (path: string) => unknown\n  set,     // (path: string, value: unknown) => void\n  update,  // (updates: Record<string, unknown>) => void\n} = useStateStore();\n```\n\n### useStateValue\n\n```typescript\nconst value = useStateValue(path: string);\n```\n\n### useStateBinding (deprecated)\n\n> **Deprecated.** Use `useBoundProp` with `$bindState` expressions instead.\n\n```typescript\nconst [value, setValue] = useStateBinding(path: string);\n```\n\n### useActions\n\n```typescript\nconst { execute } = useActions();\n// execute(binding: ActionBinding) => Promise<void>\n```\n\n### useAction\n\n```typescript\nconst { execute, isLoading } = useAction(binding: ActionBinding);\n// execute() => Promise<void>\n```\n\n### useIsVisible\n\n```typescript\nconst isVisible = useIsVisible(condition?: VisibilityCondition);\n```\n\n### useFieldValidation\n\n```typescript\nconst {\n  state,     // FieldValidationState\n  validate,  // () => ValidationResult\n  touch,     // () => void\n  clear,     // () => void\n  errors,    // string[]\n  isValid,   // boolean\n} = useFieldValidation(path: string, config?: ValidationConfig);\n```\n\n`ValidationConfig` is `{ checks?: ValidationCheck[], validateOn?: 'change' | 'blur' | 'submit' }`.\n\n### useOptionalValidation\n\nNon-throwing variant of `useValidation()`. Returns `null` when no `ValidationProvider` is present, instead of throwing. Useful in components that may or may not be rendered inside a validation context.\n\n```typescript\nconst validation = useOptionalValidation();\n// ValidationContextValue | null\n```\n\n### useBoundProp\n\nTwo-way binding helper for `$bindState` / `$bindItem` expressions. Returns `[value, setValue]` where `setValue` writes back to the bound state path.\n\n```typescript\nconst [value, setValue] = useBoundProp<T>(\n  propValue: T | undefined,       // The already-resolved prop value\n  bindingPath: string | undefined  // From bindings?.value\n);\n```\n\nUse inside registry components:\n\n```tsx\nconst Input: ComponentRenderer = ({ props, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);\n  return <input value={value ?? \"\"} onChange={(e) => setValue(e.target.value)} />;\n};\n```\n\n### Chat Hooks\n\nTwo hooks are available for chat + GenUI, depending on your setup:\n\n- **`useChatUI`** -- Self-contained chat hook with its own message state, fetch logic, and mixed stream parsing. Use when you want a standalone chat experience without the Vercel AI SDK.\n- **`useJsonRenderMessage`** -- Extracts spec + text from an AI SDK `UIMessage.parts` array. Use with the Vercel AI SDK's `useChat` for full AI SDK integration.\n\n### useChatUI\n\nHook for chat + GenUI experiences. Manages a multi-turn conversation where each assistant message can contain both text and a json-render UI spec.\n\n```typescript\nconst {\n  messages,     // ChatMessage[] - all messages in the conversation\n  isStreaming,  // boolean - true while streaming\n  error,        // Error | null\n  send,         // (text: string) => Promise<void>\n  clear,        // () => void - reset conversation\n} = useChatUI({\n  api: string,                                   // API endpoint\n  onComplete?: (message: ChatMessage) => void,   // Called when streaming completes\n  onError?: (error: Error) => void,              // Called on error\n});\n\ninterface ChatMessage {\n  id: string;\n  role: \"user\" | \"assistant\";\n  text: string;\n  spec: Spec | null;\n}\n```\n\n### useJsonRenderMessage\n\nExtract a spec and text content from an AI SDK message's `parts` array. Designed for integration with Vercel AI SDK's `useChat`.\n\n```typescript\nconst { spec, text, hasSpec } = useJsonRenderMessage(parts: DataPart[]);\n\n// spec: Spec | null     - compiled from JSONL patches in data parts\n// text: string          - concatenated text parts\n// hasSpec: boolean      - true when spec is non-null\n```\n\n### buildSpecFromParts / getTextFromParts\n\nStandalone utilities for extracting spec and text from AI SDK message parts (non-hook versions):\n\n```typescript\nimport { buildSpecFromParts, getTextFromParts } from '@json-render/react';\n\nconst spec = buildSpecFromParts(message.parts);   // Spec | null\nconst text = getTextFromParts(message.parts);      // string\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/react-email/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/react-email\")\n\n# @json-render/react-email\n\nReact Email renderer. Turn JSON specs into HTML or plain-text emails using `@react-email/components` and `@react-email/render`.\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/react-email @react-email/components @react-email/render\n```\n\nSee the [React Email example](https://github.com/vercel-labs/json-render/tree/main/examples/react-email) for a full working example.\n\n## schema\n\nThe email element schema for specs. Use with `defineCatalog` from core.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema, standardComponentDefinitions } from '@json-render/react-email';\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Render Functions\n\nServer-side functions for producing email output. All accept a spec and optional `RenderOptions`.\n\n```typescript\nimport { renderToHtml, renderToPlainText } from '@json-render/react-email';\n\nconst html = await renderToHtml(spec);\n\nconst plainText = await renderToPlainText(spec);\n```\n\n### RenderOptions\n\n```typescript\ninterface RenderOptions {\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;  // default: true\n  state?: Record<string, unknown>;\n}\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>registry</code></td>\n      <td>Custom component map (merged with standard components)</td>\n    </tr>\n    <tr>\n      <td><code>includeStandard</code></td>\n      <td>Include built-in standard components (default: <code>true</code>)</td>\n    </tr>\n    <tr>\n      <td><code>state</code></td>\n      <td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>\n    </tr>\n  </tbody>\n</table>\n\n## defineRegistry\n\nCreate a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.\n\n```tsx\nimport { defineRegistry } from '@json-render/react-email';\nimport { Container, Heading, Text } from '@react-email/components';\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <Container style={{ padding: 16, backgroundColor: '#fff' }}>\n        <Heading>{props.title}</Heading>\n        {children}\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\n## createRenderer\n\nCreate a standalone renderer component wired to state, actions, and validation (for interactive previews in the browser).\n\n```typescript\nimport { createRenderer } from '@json-render/react-email';\n\nconst EmailRenderer = createRenderer(catalog, components);\n```\n\n## Renderer\n\nThe main component that renders a spec to React Email elements. Use inside `JSONUIProvider` when you need state, actions, or visibility.\n\n```typescript\ninterface RendererProps {\n  spec: Spec | null;\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;  // default: true\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n```\n\n## Standard Components\n\n### Document structure\n\n<table>\n  <thead>\n    <tr>\n      <th>Component</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>Html</code></td>\n      <td>Top-level email wrapper. Must be the root element.</td>\n    </tr>\n    <tr>\n      <td><code>Head</code></td>\n      <td>Email head section. Place inside Html.</td>\n    </tr>\n    <tr>\n      <td><code>Body</code></td>\n      <td>Email body wrapper. Place inside Html.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Layout\n\n<table>\n  <thead>\n    <tr>\n      <th>Component</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>Container</code></td>\n      <td>Constrains content width (e.g. max-width 600px).</td>\n    </tr>\n    <tr>\n      <td><code>Section</code></td>\n      <td>Groups related content.</td>\n    </tr>\n    <tr>\n      <td><code>Row</code></td>\n      <td>Horizontal layout row.</td>\n    </tr>\n    <tr>\n      <td><code>Column</code></td>\n      <td>Column within a Row.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Content\n\n<table>\n  <thead>\n    <tr>\n      <th>Component</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>Heading</code></td>\n      <td>Heading text (h1-h6).</td>\n    </tr>\n    <tr>\n      <td><code>Text</code></td>\n      <td>Body text paragraph.</td>\n    </tr>\n    <tr>\n      <td><code>Link</code></td>\n      <td>Hyperlink with text and href.</td>\n    </tr>\n    <tr>\n      <td><code>Button</code></td>\n      <td>Call-to-action button (link styled as button).</td>\n    </tr>\n    <tr>\n      <td><code>Image</code></td>\n      <td>Image from URL.</td>\n    </tr>\n    <tr>\n      <td><code>Hr</code></td>\n      <td>Horizontal rule separator.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Utility\n\n<table>\n  <thead>\n    <tr>\n      <th>Component</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>Preview</code></td>\n      <td>Preview text for inbox (inside Html).</td>\n    </tr>\n    <tr>\n      <td><code>Markdown</code></td>\n      <td>Renders markdown content as email-safe HTML.</td>\n    </tr>\n  </tbody>\n</table>\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from '@json-render/react-email/server';\n```\n\n## Sub-path Exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>@json-render/react-email</code></td>\n      <td>Full package: schema, renderer, components, render functions</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-email/server</code></td>\n      <td>Schema and catalog definitions only (no React)</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-email/catalog</code></td>\n      <td>Standard component definitions and types</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-email/render</code></td>\n      <td>Server-side render functions only</td>\n    </tr>\n  </tbody>\n</table>\n\n## Types\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>ReactEmailSchema</code></td>\n      <td>Schema type for email specs</td>\n    </tr>\n    <tr>\n      <td><code>ReactEmailSpec</code></td>\n      <td>Spec type for email documents</td>\n    </tr>\n    <tr>\n      <td><code>RenderOptions</code></td>\n      <td>Options for render functions</td>\n    </tr>\n    <tr>\n      <td><code>ComponentContext</code></td>\n      <td>Typed component render function context</td>\n    </tr>\n    <tr>\n      <td><code>ComponentFn</code></td>\n      <td>Component render function type</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentDefinitions</code></td>\n      <td>Type of the standard component definitions object</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentProps&lt;K&gt;</code></td>\n      <td>Inferred props type for a standard component by name</td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/react-native/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/react-native\")\n\n# @json-render/react-native\n\nReact Native renderer with standard components, providers, and hooks.\n\n## Standard Components\n\n### Layout\n\n<table>\n<thead>\n<tr><th>Component</th><th>Props</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>Container</code></td><td><code>padding</code>, <code>background</code>, <code>borderRadius</code>, <code>borderColor</code>, <code>flex</code></td><td>Basic wrapper with styling</td></tr>\n<tr><td><code>Row</code></td><td><code>gap</code>, <code>align</code>, <code>justify</code>, <code>flex</code>, <code>wrap</code></td><td>Horizontal flex layout</td></tr>\n<tr><td><code>Column</code></td><td><code>gap</code>, <code>align</code>, <code>justify</code>, <code>flex</code></td><td>Vertical flex layout</td></tr>\n<tr><td><code>ScrollContainer</code></td><td><code>direction</code></td><td>Scrollable area (vertical or horizontal)</td></tr>\n<tr><td><code>SafeArea</code></td><td><code>edges</code></td><td>Safe area insets for notch/home indicator</td></tr>\n<tr><td><code>Pressable</code></td><td><code>action</code>, <code>actionParams</code></td><td>Touchable wrapper that triggers actions</td></tr>\n<tr><td><code>Spacer</code></td><td><code>size</code>, <code>flex</code></td><td>Fixed or flexible spacing</td></tr>\n<tr><td><code>Divider</code></td><td><code>color</code>, <code>thickness</code></td><td>Thin line separator</td></tr>\n</tbody>\n</table>\n\n### Content\n\n<table>\n<thead>\n<tr><th>Component</th><th>Props</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>Heading</code></td><td><code>text</code>, <code>level</code>, <code>align</code>, <code>color</code></td><td>Heading text (levels 1-6)</td></tr>\n<tr><td><code>Paragraph</code></td><td><code>text</code>, <code>align</code>, <code>color</code></td><td>Body text</td></tr>\n<tr><td><code>Label</code></td><td><code>text</code>, <code>color</code>, <code>bold</code></td><td>Small label text</td></tr>\n<tr><td><code>Image</code></td><td><code>uri</code>, <code>width</code>, <code>height</code>, <code>resizeMode</code>, <code>borderRadius</code></td><td>Image display</td></tr>\n<tr><td><code>Avatar</code></td><td><code>uri</code>, <code>size</code>, <code>fallback</code></td><td>Circular avatar</td></tr>\n<tr><td><code>Badge</code></td><td><code>label</code>, <code>color</code>, <code>textColor</code></td><td>Status badge</td></tr>\n<tr><td><code>Chip</code></td><td><code>label</code>, <code>selected</code>, <code>color</code></td><td>Tag/chip</td></tr>\n</tbody>\n</table>\n\n### Input\n\n<table>\n<thead>\n<tr><th>Component</th><th>Props</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>Button</code></td><td><code>label</code>, <code>variant</code>, <code>size</code>, <code>disabled</code>, <code>action</code>, <code>actionParams</code></td><td>Pressable button</td></tr>\n<tr><td><code>TextInput</code></td><td><code>placeholder</code>, <code>value</code> (use <code>$bindState</code>), <code>secure</code>, <code>keyboardType</code>, <code>multiline</code></td><td>Text input field</td></tr>\n<tr><td><code>Switch</code></td><td><code>checked</code> (use <code>$bindState</code>), <code>label</code></td><td>Toggle switch</td></tr>\n<tr><td><code>Checkbox</code></td><td><code>checked</code> (use <code>$bindState</code>), <code>label</code></td><td>Checkbox with label</td></tr>\n<tr><td><code>Slider</code></td><td><code>value</code> (use <code>$bindState</code>), <code>min</code>, <code>max</code>, <code>step</code></td><td>Range slider</td></tr>\n<tr><td><code>SearchBar</code></td><td><code>placeholder</code>, <code>value</code> (use <code>$bindState</code>)</td><td>Search input</td></tr>\n</tbody>\n</table>\n\n### Feedback\n\n<table>\n<thead>\n<tr><th>Component</th><th>Props</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>Spinner</code></td><td><code>size</code>, <code>color</code></td><td>Loading indicator</td></tr>\n<tr><td><code>ProgressBar</code></td><td><code>progress</code>, <code>color</code>, <code>trackColor</code></td><td>Progress indicator</td></tr>\n</tbody>\n</table>\n\n### Composite\n\n<table>\n<thead>\n<tr><th>Component</th><th>Props</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>Card</code></td><td><code>title</code>, <code>subtitle</code>, <code>padding</code></td><td>Card container</td></tr>\n<tr><td><code>ListItem</code></td><td><code>title</code>, <code>subtitle</code>, <code>leading</code>, <code>trailing</code>, <code>action</code>, <code>actionParams</code></td><td>List row</td></tr>\n<tr><td><code>Modal</code></td><td><code>visible</code>, <code>title</code></td><td>Bottom sheet modal</td></tr>\n</tbody>\n</table>\n\n## Providers\n\n### StateProvider\n\n```tsx\n<StateProvider initialState={object} onStateChange={fn}>\n  {children}\n</StateProvider>\n```\n\n<table>\n<thead>\n<tr><th>Prop</th><th>Type</th><th>Description</th></tr>\n</thead>\n<tbody>\n<tr><td><code>store</code></td><td><code>StateStore</code></td><td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td></tr>\n<tr><td><code>initialState</code></td><td><code>Record&lt;string, unknown&gt;</code></td><td>Initial state model (uncontrolled mode).</td></tr>\n<tr><td><code>onStateChange</code></td><td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td><td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td></tr>\n</tbody>\n</table>\n\n#### External Store (Controlled Mode)\n\nPass a `StateStore` to bypass the internal state and wire json-render to any state management library:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>\n  {children}\n</StateProvider>\n\n// Mutate from anywhere — components re-render automatically:\nstore.set(\"/count\", 1);\n```\n\nThe `store` prop is also available on `JSONUIProvider` and `createRenderer`.\n\n### ActionProvider\n\n```tsx\n<ActionProvider handlers={Record<string, ActionHandler>}>\n  {children}\n</ActionProvider>\n```\n\n### VisibilityProvider\n\n```tsx\n<VisibilityProvider>\n  {children}\n</VisibilityProvider>\n```\n\nConditions in specs use the `VisibilityCondition` format with `$state` paths (e.g. `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`). See [visibility](/docs/visibility) for the full syntax.\n\n### ValidationProvider\n\n```tsx\n<ValidationProvider>\n  {children}\n</ValidationProvider>\n```\n\n## defineRegistry\n\nCreate a type-safe component registry. Standard components are built-in; only register custom components.\n\n```tsx\nimport { defineRegistry, type Components } from '@json-render/react-native';\n\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\n## Hooks\n\n### useUIStream\n\n```typescript\nconst {\n  spec,         // Spec | null - current UI state\n  isStreaming,  // boolean - true while streaming\n  error,        // Error | null\n  send,         // (prompt: string) => Promise<void>\n  clear,        // () => void - reset spec and error\n} = useUIStream({\n  api: string,\n  onComplete?: (spec: Spec) => void,\n  onError?: (error: Error) => void,\n});\n```\n\n### useStateStore\n\n```typescript\nconst { state, get, set, update } = useStateStore();\n```\n\n### useStateValue\n\n```typescript\nconst value = useStateValue(path: string);\n```\n\n### useStateBinding (deprecated)\n\n> **Deprecated.** Use `useBoundProp` with `$bindState` expressions instead.\n\n```typescript\nconst [value, setValue] = useStateBinding(path: string);\n```\n\n### useActions\n\n```typescript\nconst { execute } = useActions();\n```\n\n### useIsVisible\n\n```typescript\nconst isVisible = useIsVisible(condition?: VisibilityCondition);\n```\n\n## Catalog Exports\n\n```typescript\nimport { standardComponentDefinitions, standardActionDefinitions } from \"@json-render/react-native/catalog\";\nimport { schema } from \"@json-render/react-native/schema\";\n```\n\n<table>\n<thead>\n<tr><th>Export</th><th>Purpose</th></tr>\n</thead>\n<tbody>\n<tr><td><code>standardComponentDefinitions</code></td><td>Catalog definitions for all 25+ standard components</td></tr>\n<tr><td><code>standardActionDefinitions</code></td><td>Catalog definitions for standard actions (setState, navigate)</td></tr>\n<tr><td><code>schema</code></td><td>React Native element tree schema</td></tr>\n</tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/react-pdf/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/react-pdf\")\n\n# @json-render/react-pdf\n\nPDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\nSee the [React PDF example](https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf) for a full working example.\n\n## schema\n\nThe PDF element schema for document specs. Use with `defineCatalog` from core.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema, standardComponentDefinitions } from '@json-render/react-pdf';\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n```\n\n## Render Functions\n\nServer-side functions for producing PDF output. All accept a spec and optional `RenderOptions`.\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from '@json-render/react-pdf';\n\nconst buffer = await renderToBuffer(spec);\n\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\nawait renderToFile(spec, './output.pdf');\n```\n\n### RenderOptions\n\n```typescript\ninterface RenderOptions {\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;  // default: true\n  state?: Record<string, unknown>;\n}\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>registry</code></td>\n      <td>Custom component map (merged with standard components)</td>\n    </tr>\n    <tr>\n      <td><code>includeStandard</code></td>\n      <td>Include built-in standard components (default: <code>true</code>)</td>\n    </tr>\n    <tr>\n      <td><code>state</code></td>\n      <td>Initial state for <code>$state</code> / <code>$cond</code> dynamic prop resolution</td>\n    </tr>\n  </tbody>\n</table>\n\n## defineRegistry\n\nCreate a type-safe component registry from a catalog. Components receive `{ props, children, emit, bindings, loading }`.\n\n```tsx\nimport { defineRegistry } from '@json-render/react-pdf';\nimport { View, Text } from '@react-pdf/renderer';\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Badge: ({ props }) => (\n      <View style={{ backgroundColor: props.color ?? '#e5e7eb', padding: 4, borderRadius: 4 }}>\n        <Text style={{ fontSize: 10 }}>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## createRenderer\n\nCreate a standalone renderer component wired to state, actions, and validation.\n\n```typescript\nimport { createRenderer } from '@json-render/react-pdf';\n\nconst PDFRenderer = createRenderer(catalog, components);\n```\n\n```typescript\ninterface CreateRendererProps {\n  spec: Spec | null;\n  store?: StateStore;\n  state?: Record<string, unknown>;\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n```\n\nWhen `store` is provided, `state` and `onStateChange` are ignored (controlled mode).\n\n## Renderer\n\nThe main component that renders a spec to `@react-pdf/renderer` elements.\n\n```typescript\ninterface RendererProps {\n  spec: Spec | null;\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;  // default: true\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n```\n\n## Standard Components\n\n### Document Structure\n\n#### Document\n\nTop-level PDF wrapper. Must be the root element. Children must be `Page` components.\n\n```typescript\n{\n  title: string | null;\n  author: string | null;\n  subject: string | null;\n}\n```\n\n#### Page\n\nA page in the document with configurable size, orientation, and margins.\n\n```typescript\n{\n  size: \"A4\" | \"A3\" | \"A5\" | \"LETTER\" | \"LEGAL\" | \"TABLOID\" | null;\n  orientation: \"portrait\" | \"landscape\" | null;\n  marginTop: number | null;\n  marginBottom: number | null;\n  marginLeft: number | null;\n  marginRight: number | null;\n  backgroundColor: string | null;\n}\n```\n\n### Layout\n\n#### View\n\nGeneric container with padding, margin, background, border, and flex alignment.\n\n```typescript\n{\n  padding: number | null;\n  paddingTop: number | null;\n  paddingBottom: number | null;\n  paddingLeft: number | null;\n  paddingRight: number | null;\n  margin: number | null;\n  backgroundColor: string | null;\n  borderWidth: number | null;\n  borderColor: string | null;\n  borderRadius: number | null;\n  flex: number | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n}\n```\n\n#### Row\n\nHorizontal flex layout with optional wrapping.\n\n```typescript\n{\n  gap: number | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n  padding: number | null;\n  flex: number | null;\n  wrap: boolean | null;\n}\n```\n\n#### Column\n\nVertical flex layout.\n\n```typescript\n{\n  gap: number | null;\n  alignItems: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | null;\n  justifyContent: \"flex-start\" | \"center\" | \"flex-end\" | \"space-between\" | \"space-around\" | null;\n  padding: number | null;\n  flex: number | null;\n}\n```\n\n### Content\n\n#### Heading\n\nh1-h4 heading text with configurable color and alignment.\n\n```typescript\n{\n  text: string;\n  level: \"h1\" | \"h2\" | \"h3\" | \"h4\" | null;\n  color: string | null;\n  align: \"left\" | \"center\" | \"right\" | null;\n}\n```\n\n#### Text\n\nBody text with full styling control.\n\n```typescript\n{\n  text: string;\n  fontSize: number | null;\n  color: string | null;\n  align: \"left\" | \"center\" | \"right\" | null;\n  fontWeight: \"normal\" | \"bold\" | null;\n  fontStyle: \"normal\" | \"italic\" | null;\n  lineHeight: number | null;\n}\n```\n\n#### Image\n\nImage from a URL with optional dimensions and fit.\n\n```typescript\n{\n  src: string;\n  width: number | null;\n  height: number | null;\n  objectFit: \"contain\" | \"cover\" | \"fill\" | \"none\" | null;\n}\n```\n\n#### Link\n\nHyperlink with visible text.\n\n```typescript\n{\n  text: string;\n  href: string;\n  fontSize: number | null;\n  color: string | null;\n}\n```\n\n### Data\n\n#### Table\n\nData table with typed columns and string rows. Supports header styling and striped rows.\n\n```typescript\n{\n  columns: { header: string; width?: string; align?: \"left\" | \"center\" | \"right\" }[];\n  rows: string[][];\n  headerBackgroundColor: string | null;\n  headerTextColor: string | null;\n  borderColor: string | null;\n  fontSize: number | null;\n  striped: boolean | null;\n}\n```\n\n#### List\n\nOrdered or unordered list.\n\n```typescript\n{\n  items: string[];\n  ordered: boolean | null;\n  fontSize: number | null;\n  color: string | null;\n  spacing: number | null;\n}\n```\n\n### Decorative\n\n#### Divider\n\nHorizontal line separator.\n\n```typescript\n{\n  color: string | null;\n  thickness: number | null;\n  marginTop: number | null;\n  marginBottom: number | null;\n}\n```\n\n#### Spacer\n\nEmpty vertical space.\n\n```typescript\n{\n  height: number | null;\n}\n```\n\n### Page-Level\n\n#### PageNumber\n\nRenders current page number and total pages. Format uses `{pageNumber}` and `{totalPages}` placeholders.\n\n```typescript\n{\n  format: string | null;    // default: \"{pageNumber} / {totalPages}\"\n  fontSize: number | null;\n  color: string | null;\n  align: \"left\" | \"center\" | \"right\" | null;\n}\n```\n\n## External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider`, `JSONUIProvider`, or `createRenderer` for full control over state:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\nstore.set(\"/invoice/total\", 200);\n```\n\nWhen `store` is provided, `initialState` / `state` and `onStateChange` are ignored.\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React or `@react-pdf/renderer`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from '@json-render/react-pdf/server';\n```\n\n## Sub-path Exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>@json-render/react-pdf</code></td>\n      <td>Full package: schema, renderer, components, render functions</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-pdf/server</code></td>\n      <td>Schema and catalog definitions only (no React)</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-pdf/catalog</code></td>\n      <td>Standard component definitions and types</td>\n    </tr>\n    <tr>\n      <td><code>@json-render/react-pdf/render</code></td>\n      <td>Server-side render functions only</td>\n    </tr>\n  </tbody>\n</table>\n\n## Types\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>ReactPdfSchema</code></td>\n      <td>Schema type for PDF specs</td>\n    </tr>\n    <tr>\n      <td><code>ReactPdfSpec</code></td>\n      <td>Spec type for PDF documents</td>\n    </tr>\n    <tr>\n      <td><code>RenderOptions</code></td>\n      <td>Options for render functions</td>\n    </tr>\n    <tr>\n      <td><code>ComponentContext</code></td>\n      <td>Typed component render function context</td>\n    </tr>\n    <tr>\n      <td><code>ComponentFn</code></td>\n      <td>Component render function type</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentDefinitions</code></td>\n      <td>Type of the standard component definitions object</td>\n    </tr>\n    <tr>\n      <td><code>StandardComponentProps&lt;K&gt;</code></td>\n      <td>Inferred props type for a standard component by name</td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/react-three-fiber/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/react-three-fiber\")\n\n# @json-render/react-three-fiber\n\nReact Three Fiber renderer for json-render. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.\n\n## Installation\n\n```bash\nnpm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod\n```\n\n## Entry Points\n\n<table>\n<thead>\n<tr>\n<th>Entry Point</th>\n<th>Exports</th>\n<th>Use For</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>@json-render/react-three-fiber</code></td>\n<td><code>threeComponents</code>, <code>ThreeRenderer</code>, <code>ThreeCanvas</code>, schemas</td>\n<td>React Three Fiber implementations and renderer</td>\n</tr>\n<tr>\n<td><code>@json-render/react-three-fiber/catalog</code></td>\n<td><code>threeComponentDefinitions</code></td>\n<td>Catalog schemas (no R3F dependency, safe for server)</td>\n</tr>\n</tbody>\n</table>\n\n## Usage\n\n```tsx\nimport {'{ defineCatalog }'} from \"@json-render/core\";\nimport {'{ schema, defineRegistry }'} from \"@json-render/react\";\nimport {'{'}\n  threeComponentDefinitions,\n  threeComponents,\n  ThreeCanvas,\n{'}'} from \"@json-render/react-three-fiber\";\n\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\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### ThreeCanvas (convenience)\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</Canvas>\n```\n\n## Components\n\n### Primitives\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n<th>Key Props</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Box</code></td>\n<td>Box mesh (default 1x1x1)</td>\n<td><code>width</code>, <code>height</code>, <code>depth</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Sphere</code></td>\n<td>Sphere mesh</td>\n<td><code>radius</code>, <code>widthSegments</code>, <code>heightSegments</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Cylinder</code></td>\n<td>Cylinder mesh</td>\n<td><code>radiusTop</code>, <code>radiusBottom</code>, <code>height</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Cone</code></td>\n<td>Cone mesh</td>\n<td><code>radius</code>, <code>height</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Torus</code></td>\n<td>Torus (donut) mesh</td>\n<td><code>radius</code>, <code>tube</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Plane</code></td>\n<td>Flat plane mesh</td>\n<td><code>width</code>, <code>height</code>, <code>material</code></td>\n</tr>\n<tr>\n<td><code>Capsule</code></td>\n<td>Capsule mesh</td>\n<td><code>radius</code>, <code>length</code>, <code>material</code></td>\n</tr>\n</tbody>\n</table>\n\nAll primitives share: <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>castShadow</code>, <code>receiveShadow</code>, <code>material</code>.\n\n### Material Schema\n\n<table>\n<thead>\n<tr>\n<th>Property</th>\n<th>Type</th>\n<th>Default</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>color</code></td>\n<td><code>string</code></td>\n<td><code>\"#ffffff\"</code></td>\n</tr>\n<tr>\n<td><code>metalness</code></td>\n<td><code>number</code></td>\n<td><code>0</code></td>\n</tr>\n<tr>\n<td><code>roughness</code></td>\n<td><code>number</code></td>\n<td><code>1</code></td>\n</tr>\n<tr>\n<td><code>emissive</code></td>\n<td><code>string</code></td>\n<td><code>\"#000000\"</code></td>\n</tr>\n<tr>\n<td><code>emissiveIntensity</code></td>\n<td><code>number</code></td>\n<td><code>1</code></td>\n</tr>\n<tr>\n<td><code>opacity</code></td>\n<td><code>number</code></td>\n<td><code>1</code></td>\n</tr>\n<tr>\n<td><code>transparent</code></td>\n<td><code>boolean</code></td>\n<td><code>false</code></td>\n</tr>\n<tr>\n<td><code>wireframe</code></td>\n<td><code>boolean</code></td>\n<td><code>false</code></td>\n</tr>\n</tbody>\n</table>\n\n### Lights\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n<th>Key Props</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>AmbientLight</code></td>\n<td>Uniform illumination</td>\n<td><code>color</code>, <code>intensity</code></td>\n</tr>\n<tr>\n<td><code>DirectionalLight</code></td>\n<td>Sunlight-style</td>\n<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>castShadow</code></td>\n</tr>\n<tr>\n<td><code>PointLight</code></td>\n<td>Radiates from a point</td>\n<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>distance</code>, <code>decay</code></td>\n</tr>\n<tr>\n<td><code>SpotLight</code></td>\n<td>Cone of light</td>\n<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>angle</code>, <code>penumbra</code></td>\n</tr>\n</tbody>\n</table>\n\n### Other Components\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n<th>Key Props</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Group</code></td>\n<td>Container for children</td>\n<td><code>position</code>, <code>rotation</code>, <code>scale</code></td>\n</tr>\n<tr>\n<td><code>Model</code></td>\n<td>GLTF/GLB model loader</td>\n<td><code>url</code>, <code>position</code>, <code>rotation</code>, <code>scale</code></td>\n</tr>\n<tr>\n<td><code>Environment</code></td>\n<td>HDRI environment map</td>\n<td><code>preset</code>, <code>background</code>, <code>blur</code>, <code>intensity</code></td>\n</tr>\n<tr>\n<td><code>Fog</code></td>\n<td>Linear fog effect</td>\n<td><code>color</code>, <code>near</code>, <code>far</code></td>\n</tr>\n<tr>\n<td><code>GridHelper</code></td>\n<td>Reference grid</td>\n<td><code>size</code>, <code>divisions</code>, <code>color</code></td>\n</tr>\n<tr>\n<td><code>Text3D</code></td>\n<td>3D text (SDF)</td>\n<td><code>text</code>, <code>fontSize</code>, <code>color</code>, <code>anchorX</code>, <code>anchorY</code></td>\n</tr>\n<tr>\n<td><code>PerspectiveCamera</code></td>\n<td>Camera</td>\n<td><code>position</code>, <code>fov</code>, <code>near</code>, <code>far</code>, <code>makeDefault</code></td>\n</tr>\n<tr>\n<td><code>OrbitControls</code></td>\n<td>Camera controls</td>\n<td><code>enableDamping</code>, <code>enableZoom</code>, <code>autoRotate</code></td>\n</tr>\n</tbody>\n</table>\n\n## Shared Schemas\n\nReusable Zod schemas for custom 3D components:\n\n```tsx\nimport {'{ vector3Schema, materialSchema, transformProps, shadowProps }'} from \"@json-render/react-three-fiber\";\n```\n\n<table>\n<thead>\n<tr>\n<th>Export</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>vector3Schema</code></td>\n<td><code>z.tuple([z.number(), z.number(), z.number()])</code></td>\n</tr>\n<tr>\n<td><code>materialSchema</code></td>\n<td>Standard material props (color, metalness, roughness, etc.)</td>\n</tr>\n<tr>\n<td><code>transformProps</code></td>\n<td><code>{'{ position, rotation, scale }'}</code> schema fields</td>\n</tr>\n<tr>\n<td><code>shadowProps</code></td>\n<td><code>{'{ castShadow, receiveShadow }'}</code> schema fields</td>\n</tr>\n</tbody>\n</table>\n\n## ThreeRenderer\n\n<table>\n<thead>\n<tr>\n<th>Prop</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>spec</code></td>\n<td><code>Spec | null</code></td>\n<td>The spec to render as a 3D scene</td>\n</tr>\n<tr>\n<td><code>registry</code></td>\n<td><code>ComponentRegistry</code></td>\n<td>Component registry from <code>defineRegistry</code></td>\n</tr>\n<tr>\n<td><code>store</code></td>\n<td><code>StateStore</code></td>\n<td>External state store (controlled mode)</td>\n</tr>\n<tr>\n<td><code>initialState</code></td>\n<td><code>Record&lt;string, unknown&gt;</code></td>\n<td>Initial state (uncontrolled mode)</td>\n</tr>\n<tr>\n<td><code>handlers</code></td>\n<td><code>Record&lt;string, Function&gt;</code></td>\n<td>Action handlers</td>\n</tr>\n<tr>\n<td><code>loading</code></td>\n<td><code>boolean</code></td>\n<td>Whether the spec is streaming</td>\n</tr>\n<tr>\n<td><code>children</code></td>\n<td><code>ReactNode</code></td>\n<td>Additional R3F elements alongside the spec</td>\n</tr>\n</tbody>\n</table>\n\n## ThreeCanvas\n\nExtends <code>ThreeRendererProps</code> with Canvas options:\n\n<table>\n<thead>\n<tr>\n<th>Prop</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>shadows</code></td>\n<td><code>boolean</code></td>\n<td>Enable shadow maps</td>\n</tr>\n<tr>\n<td><code>camera</code></td>\n<td><code>object</code></td>\n<td>Default camera config (position, fov, etc.)</td>\n</tr>\n<tr>\n<td><code>className</code></td>\n<td><code>string</code></td>\n<td>CSS class for the canvas container</td>\n</tr>\n<tr>\n<td><code>style</code></td>\n<td><code>CSSProperties</code></td>\n<td>Inline styles for the canvas container</td>\n</tr>\n</tbody>\n</table>\n\n## Type Helpers\n\n```tsx\nimport type {'{ ThreeProps }'} from \"@json-render/react-three-fiber\";\n\ntype BoxProps = ThreeProps<\"Box\">;\ntype SphereProps = ThreeProps<\"Sphere\">;\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/redux/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/redux\")\n\n# @json-render/redux\n\nRedux / Redux Toolkit adapter for json-render's `StateStore` interface.\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## reduxStateStore\n\nCreate a `StateStore` backed by a Redux store.\n\n```typescript\nimport { reduxStateStore } from \"@json-render/redux\";\n```\n\n### Options\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Required</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>store</code></td>\n      <td><code>Store</code></td>\n      <td>Yes</td>\n      <td>The Redux store instance.</td>\n    </tr>\n    <tr>\n      <td><code>selector</code></td>\n      <td><code>{'(state: S) => StateModel'}</code></td>\n      <td>No</td>\n      <td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>\n    </tr>\n    <tr>\n      <td><code>dispatch</code></td>\n      <td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>\n      <td>Yes</td>\n      <td>Dispatch an action that replaces the selected slice with the next state.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Example\n\n```typescript\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { StateProvider } from \"@json-render/react\";\n\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\nconst reduxStore = configureStore({\n  reducer: { ui: uiSlice.reducer },\n});\n\nconst store = reduxStateStore({\n  store: reduxStore,\n  selector: (state) => state.ui,\n  dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n});\n```\n\n```tsx\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Redux */}\n</StateProvider>\n```\n\n## Re-exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Source</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>StateStore</code></td>\n      <td><code>@json-render/core</code></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/remotion/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/remotion\")\n\n# @json-render/remotion\n\nRemotion video renderer. Turn JSON timeline specs into video compositions.\n\n## schema\n\nThe timeline schema for video specs. Use with `defineCatalog` from core.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema, standardComponentDefinitions } from '@json-render/remotion';\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n  transitions: standardTransitionDefinitions,\n  effects: standardEffectDefinitions,\n});\n```\n\n## Renderer\n\nThe main composition component that renders timeline specs. Use with Remotion's Player or in a Remotion project.\n\n```tsx\nimport { Player } from '@remotion/player';\nimport { Renderer } from '@json-render/remotion';\n\nfunction VideoPlayer({ spec }) {\n  return (\n    <Player\n      component={Renderer}\n      inputProps={{ spec }}\n      durationInFrames={spec.composition.durationInFrames}\n      fps={spec.composition.fps}\n      compositionWidth={spec.composition.width}\n      compositionHeight={spec.composition.height}\n      controls\n    />\n  );\n}\n```\n\n### Custom Components\n\nPass custom components to the Renderer:\n\n```tsx\nimport { Renderer, standardComponents } from '@json-render/remotion';\n\nconst customComponents = {\n  ...standardComponents,\n  MyCustomClip: ({ clip }) => <div>{clip.props.text}</div>,\n};\n\n<Player\n  component={Renderer}\n  inputProps={{ spec, components: customComponents }}\n  // ...\n/>\n```\n\n## Standard Components\n\nPre-built video components included in the package:\n\n```typescript\nimport {\n  TitleCard,      // Full-screen title with subtitle\n  ImageSlide,     // Full-screen image display\n  SplitScreen,    // Two-column layout\n  QuoteCard,      // Quote with attribution\n  StatCard,       // Large statistic display\n  LowerThird,     // Name/title overlay\n  TextOverlay,    // Centered text overlay\n  TypingText,     // Terminal typing animation\n  LogoBug,        // Corner logo watermark\n  VideoClip,      // Video playback\n} from '@json-render/remotion';\n```\n\n### TitleCard Props\n\n```typescript\n{\n  title: string;\n  subtitle?: string;\n  backgroundColor?: string;  // default: \"#1a1a1a\"\n  textColor?: string;        // default: \"#ffffff\"\n}\n```\n\n### TypingText Props\n\n```typescript\n{\n  text: string;\n  charsPerSecond?: number;   // default: 15\n  showCursor?: boolean;      // default: true\n  cursorChar?: string;       // default: \"|\"\n  fontFamily?: string;       // default: \"monospace\"\n  fontSize?: number;         // default: 48\n  textColor?: string;        // default: \"#00ff00\"\n  backgroundColor?: string;  // default: \"#1e1e1e\"\n}\n```\n\n## Catalog Definitions\n\nPre-built definitions for creating catalogs:\n\n```typescript\nimport {\n  standardComponentDefinitions,   // All standard component definitions\n  standardTransitionDefinitions,  // fade, slideLeft, slideRight, etc.\n  standardEffectDefinitions,      // kenBurns, pulseGlow, colorShift\n} from '@json-render/remotion';\n\n// Use in your catalog\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    // Add custom components\n  },\n  transitions: standardTransitionDefinitions,\n  effects: standardEffectDefinitions,\n});\n```\n\n## Hooks & Utilities\n\n### useTransition\n\nCalculate transition styles for a clip based on current frame:\n\n```typescript\nimport { useTransition } from '@json-render/remotion';\nimport { useCurrentFrame } from 'remotion';\n\nfunction MyComponent({ clip }) {\n  const frame = useCurrentFrame();\n  const transition = useTransition(clip, frame);\n  \n  return (\n    <div style={{\n      opacity: transition.opacity,\n      transform: transition.transform,\n    }}>\n      Content\n    </div>\n  );\n}\n```\n\n### ClipWrapper\n\nAutomatically apply transitions to clip content:\n\n```tsx\nimport { ClipWrapper } from '@json-render/remotion';\n\nfunction MyClip({ clip }) {\n  return (\n    <ClipWrapper clip={clip}>\n      <div>My content with automatic transitions</div>\n    </ClipWrapper>\n  );\n}\n```\n\n## Types\n\n### TimelineSpec\n\n```typescript\ninterface TimelineSpec {\n  composition: {\n    id: string;\n    fps: number;\n    width: number;\n    height: number;\n    durationInFrames: number;\n  };\n  tracks: Track[];\n  clips: Clip[];\n  audio: {\n    tracks: AudioTrack[];\n  };\n}\n```\n\n### Clip\n\n```typescript\ninterface Clip {\n  id: string;\n  trackId: string;\n  component: string;\n  props: Record<string, unknown>;\n  from: number;\n  durationInFrames: number;\n  transitionIn?: {\n    type: string;\n    durationInFrames: number;\n  };\n  transitionOut?: {\n    type: string;\n    durationInFrames: number;\n  };\n}\n```\n\n### TransitionStyles\n\n```typescript\ninterface TransitionStyles {\n  opacity: number;\n  transform: string;\n}\n```\n\n### ComponentRegistry\n\n```typescript\ntype ClipComponent = React.ComponentType<{ clip: Clip }>;\ntype ComponentRegistry = Record<string, ClipComponent>;\n```\n\n## Transitions\n\nAvailable transition types:\n\n- `fade` - Opacity fade in/out\n- `slideLeft` - Slide from right\n- `slideRight` - Slide from left\n- `slideUp` - Slide from bottom\n- `slideDown` - Slide from top\n- `zoom` - Scale zoom in/out\n- `wipe` - Horizontal wipe\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/shadcn/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/shadcn\")\n\n# @json-render/shadcn\n\nPre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.\n\n## Installation\n\n```bash\nnpm install @json-render/shadcn @json-render/core @json-render/react zod\n```\n\nYour app must have Tailwind CSS configured.\n\n## Entry Points\n\n<table>\n<thead>\n<tr>\n<th>Entry Point</th>\n<th>Exports</th>\n<th>Use For</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>@json-render/shadcn</code></td>\n<td><code>shadcnComponents</code></td>\n<td>React implementations</td>\n</tr>\n<tr>\n<td><code>@json-render/shadcn/catalog</code></td>\n<td><code>shadcnComponentDefinitions</code></td>\n<td>Catalog schemas (no React dependency, safe for server)</td>\n</tr>\n</tbody>\n</table>\n\n## Usage\n\nPick the components you need from the standard definitions:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Heading: shadcnComponentDefinitions.Heading,\n    Button: shadcnComponentDefinitions.Button,\n    Input: shadcnComponentDefinitions.Input,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Heading: shadcnComponents.Heading,\n    Button: shadcnComponents.Button,\n    Input: shadcnComponents.Input,\n  },\n});\n```\n\nState actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. You don't need to declare them in your catalog.\n\n## Extending with Custom Components\n\nAdd custom components alongside standard ones:\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    // Standard\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Button: shadcnComponentDefinitions.Button,\n\n    // Custom\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        trend: z.enum([\"up\", \"down\", \"neutral\"]).nullable(),\n      }),\n      description: \"KPI metric display\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Button: shadcnComponents.Button,\n    Metric: ({ props }) => (\n      <div>\n        <span>{props.label}</span>\n        <span>{props.value}</span>\n      </div>\n    ),\n  },\n});\n```\n\n## Available Components\n\n### Layout\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Card</code></td>\n<td>Container card with optional title, description, maxWidth, centered</td>\n</tr>\n<tr>\n<td><code>Stack</code></td>\n<td>Flex container with direction, gap, align, justify</td>\n</tr>\n<tr>\n<td><code>Grid</code></td>\n<td>Grid layout with columns (1-6) and gap</td>\n</tr>\n<tr>\n<td><code>Separator</code></td>\n<td>Visual separator line with orientation</td>\n</tr>\n</tbody>\n</table>\n\n### Navigation\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Tabs</code></td>\n<td>Tabbed navigation with tabs array, defaultValue, value</td>\n</tr>\n<tr>\n<td><code>Accordion</code></td>\n<td>Collapsible sections with items array and type (single/multiple)</td>\n</tr>\n<tr>\n<td><code>Collapsible</code></td>\n<td>Single collapsible section with title and defaultOpen</td>\n</tr>\n<tr>\n<td><code>Pagination</code></td>\n<td>Page navigation with totalPages and page</td>\n</tr>\n</tbody>\n</table>\n\n### Overlay\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Dialog</code></td>\n<td>Modal dialog with title, description, openPath</td>\n</tr>\n<tr>\n<td><code>Drawer</code></td>\n<td>Bottom drawer with title, description, openPath</td>\n</tr>\n<tr>\n<td><code>Tooltip</code></td>\n<td>Hover tooltip with content and text</td>\n</tr>\n<tr>\n<td><code>Popover</code></td>\n<td>Click-triggered popover with trigger and content</td>\n</tr>\n<tr>\n<td><code>DropdownMenu</code></td>\n<td>Dropdown menu with label and items array</td>\n</tr>\n</tbody>\n</table>\n\n### Content\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Heading</code></td>\n<td>Heading text with level (h1-h4)</td>\n</tr>\n<tr>\n<td><code>Text</code></td>\n<td>Paragraph with variant (body, caption, muted, lead, code)</td>\n</tr>\n<tr>\n<td><code>Image</code></td>\n<td>Image with alt, width, height</td>\n</tr>\n<tr>\n<td><code>Avatar</code></td>\n<td>User avatar with src, name, size</td>\n</tr>\n<tr>\n<td><code>Badge</code></td>\n<td>Status badge with text and variant</td>\n</tr>\n<tr>\n<td><code>Alert</code></td>\n<td>Alert banner with title, message, type</td>\n</tr>\n<tr>\n<td><code>Carousel</code></td>\n<td>Horizontally scrollable carousel with items</td>\n</tr>\n<tr>\n<td><code>Table</code></td>\n<td>Data table with columns and rows</td>\n</tr>\n</tbody>\n</table>\n\n### Feedback\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Progress</code></td>\n<td>Progress bar with value, max, label</td>\n</tr>\n<tr>\n<td><code>Skeleton</code></td>\n<td>Loading placeholder with width, height, rounded</td>\n</tr>\n<tr>\n<td><code>Spinner</code></td>\n<td>Loading spinner with size and label</td>\n</tr>\n</tbody>\n</table>\n\n### Input\n\n<table>\n<thead>\n<tr>\n<th>Component</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>Button</code></td>\n<td>Clickable button with label, variant, disabled</td>\n</tr>\n<tr>\n<td><code>Link</code></td>\n<td>Anchor link with label and href</td>\n</tr>\n<tr>\n<td><code>Input</code></td>\n<td>Text input with label, name, type, placeholder, value, checks</td>\n</tr>\n<tr>\n<td><code>Textarea</code></td>\n<td>Multi-line text input with label, name, placeholder, rows, value, checks</td>\n</tr>\n<tr>\n<td><code>Select</code></td>\n<td>Dropdown select with label, name, options, value, checks</td>\n</tr>\n<tr>\n<td><code>Checkbox</code></td>\n<td>Checkbox with label, name, checked</td>\n</tr>\n<tr>\n<td><code>Radio</code></td>\n<td>Radio button group with label, name, options, value</td>\n</tr>\n<tr>\n<td><code>Switch</code></td>\n<td>Toggle switch with label, name, checked</td>\n</tr>\n<tr>\n<td><code>Slider</code></td>\n<td>Range slider with label, min, max, step, value</td>\n</tr>\n<tr>\n<td><code>Toggle</code></td>\n<td>Toggle button with label, pressed, variant</td>\n</tr>\n<tr>\n<td><code>ToggleGroup</code></td>\n<td>Group of toggle buttons with items, type, value</td>\n</tr>\n<tr>\n<td><code>ButtonGroup</code></td>\n<td>Group of buttons with buttons array and selected</td>\n</tr>\n</tbody>\n</table>\n\n## Notes\n\n- The `/catalog` entry point has no React dependency -- use it for server-side prompt generation\n- Components use Tailwind CSS classes -- your app must have Tailwind configured\n- Component implementations use bundled shadcn/ui primitives (not your app's `components/ui/`)\n- Form inputs support `checks` for validation (type + message pairs)\n- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/solid/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\nexport const metadata = pageMetadata(\"docs/api/solid\");\n\n# @json-render/solid\n\nSolidJS components, providers, and hooks for rendering json-render specs.\n\n## Installation\n\n<PackageInstall packages=\"@json-render/core @json-render/solid\" />\n\nPeer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.\n\n<PackageInstall packages=\"solid-js zod\" />\n\n## Providers\n\n### StateProvider\n\n```tsx\n<StateProvider\n  initialState={{}}\n  onStateChange={(changes) => console.log(changes)}\n>\n  {/* children */}\n</StateProvider>\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Prop</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>\n        <code>store</code>\n      </td>\n      <td>\n        <code>StateStore</code>\n      </td>\n      <td>\n        External store (controlled mode). When provided,{\" \"}\n        <code>initialState</code> and <code>onStateChange</code> are ignored.\n      </td>\n    </tr>\n    <tr>\n      <td>\n        <code>initialState</code>\n      </td>\n      <td>\n        <code>Record&lt;string, unknown&gt;</code>\n      </td>\n      <td>Initial state model for uncontrolled mode.</td>\n    </tr>\n    <tr>\n      <td>\n        <code>onStateChange</code>\n      </td>\n      <td>\n        <code>\n          {\"(changes: Array<{ path: string; value: unknown }>) => void\"}\n        </code>\n      </td>\n      <td>Called for uncontrolled state updates.</td>\n    </tr>\n  </tbody>\n</table>\n\n### ActionProvider\n\n```tsx\n<ActionProvider\n  handlers={{ submit: async (params) => {} }}\n  navigate={(path) => {}}\n>\n  {/* children */}\n</ActionProvider>\n```\n\n### VisibilityProvider\n\n```tsx\n<VisibilityProvider>{/* children */}</VisibilityProvider>\n```\n\n### ValidationProvider\n\n```tsx\n<ValidationProvider customFunctions={{ custom: (value) => Boolean(value) }}>\n  {/* children */}\n</ValidationProvider>\n```\n\n### JSONUIProvider\n\nCombined provider wrapper for state, visibility, validation, and actions.\n\n```tsx\n<JSONUIProvider\n  registry={registry}\n  initialState={{}}\n  handlers={handlers}\n  validationFunctions={validationFunctions}\n>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\n## defineRegistry\n\nCreate a typed component registry and action helpers from a catalog.\n\n```tsx\nconst { registry, handlers, executeAction } = defineRegistry(catalog, {\n  components: {\n    Card: (renderProps) => <div>{renderProps.children}</div>,\n    Button: (renderProps) => (\n      <button onClick={() => renderProps.emit(\"press\")}>\n        {renderProps.element.props.label as string}\n      </button>\n    ),\n  },\n  actions: {\n    submit: async (params, setState, state) => {\n      // custom action logic\n    },\n  },\n});\n```\n\n## Components\n\n### Renderer\n\n```tsx\n<Renderer spec={spec} registry={registry} loading={false} />\n```\n\nRenders a `Spec` tree using your registry.\n\n### createRenderer\n\nBuild an app-level renderer from catalog + components:\n\n```tsx\nconst AppRenderer = createRenderer(catalog, {\n  Card: (renderProps) => <div>{renderProps.children}</div>,\n});\n\n<AppRenderer spec={spec} state={{}} onAction={(name, params) => {}} />;\n```\n\n## Hooks\n\n- `useStateStore()`\n- `useStateValue(path)` - returns an accessor\n- `useStateBinding(path)` - returns `[Accessor<T | undefined>, setValue]`\n- `useVisibility()` / `useIsVisible(condition)`\n- `useActions()` / `useAction(binding)`\n- `useValidation()` / `useOptionalValidation()`\n- `useFieldValidation(path, config)` - returns accessor-backed `state`, `errors`, and `isValid`\n- `useBoundProp(value, bindingPath)`\n- `useUIStream(options)`\n- `useChatUI(options)`\n\n## Built-in Actions\n\n`ActionProvider` handles these built-in actions:\n\n- `setState`\n- `pushState`\n- `removeState`\n- `validateForm`\n\n## Component Props\n\nRegistry components receive:\n\n```ts\ninterface ComponentRenderProps<P = Record<string, unknown>> {\n  element: UIElement<string, P>;\n  children?: JSX.Element;\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n```\n\nUse `emit(\"event\")` to dispatch event bindings. Use `on(\"event\")` to access `EventHandle` metadata (`bound`, `shouldPreventDefault`, `emit`).\n\n## Reactivity Notes\n\n- Keep changing reads in JSX expressions, `createMemo`, or `createEffect`.\n- Avoid props destructuring in component signatures when you need live updates.\n- `StateProvider` and other contexts expose getter-backed values so consumers read live signals.\n- `useStateValue`, `useStateBinding`, and `useFieldValidation` expose reactive accessors; call them as functions.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/svelte/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/svelte\")\n\n# @json-render/svelte\n\nSvelte 5 components, providers, and helpers for rendering json-render specs.\n\n## Installation\n\n<PackageInstall packages=\"@json-render/core @json-render/svelte\" />\n\nPeer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.\n\n<PackageInstall packages=\"svelte zod\" />\n\n## Components\n\n### Renderer\n\n```svelte\n<Renderer\n  spec={spec}       // Spec | null\n  registry={registry}\n  loading={false}\n/>\n```\n\nRenders a spec with your component registry. If `spec` is `null`, it renders nothing.\n\n### JsonUIProvider\n\nConvenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.\n\n```svelte\n<JsonUIProvider\n  initialState={{}}\n  handlers={handlers}\n  validationFunctions={validationFunctions}\n>\n  <Renderer {spec} {registry} />\n</JsonUIProvider>\n```\n\n## defineRegistry\n\nCreate a typed component registry and action handlers from a catalog.\n\n```typescript\nimport { defineRegistry } from \"@json-render/svelte\";\n\nconst { registry, handlers, executeAction } = defineRegistry(catalog, {\n  components: {\n    Card,\n    Button,\n  },\n  actions: {\n    submit: async (params, setState, state) => {\n      // custom action logic\n    },\n  },\n});\n```\n\n`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.\n\n## Component Props\n\nRegistry components receive `BaseComponentProps<TProps>`:\n\n```typescript\ninterface BaseComponentProps<TProps> {\n  props: TProps;\n  children?: Snippet;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n```\n\nUse `emit(\"eventName\")` to trigger handlers declared in the spec `on` bindings.\n\n## Context Helpers\n\nUse these helpers inside Svelte components:\n\n- `getStateValue(path)` - read/write state via `.current`\n- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values\n- `isVisible(condition)` - evaluate visibility via `.current`\n- `getAction(name)` - read a registered action handler via `.current`\n- `getFieldValidation(ctx, path, config)` - get field validation state + actions\n\nFor advanced usage, access full contexts:\n\n- `getStateContext()`\n- `getActionContext()`\n- `getVisibilityContext()`\n- `getValidationContext()`\n- `getOptionalValidationContext()`\n\n## Streaming\n\n### createUIStream\n\n```typescript\nconst stream = createUIStream({\n  api: \"/api/generate-ui\",\n  onComplete: (spec) => console.log(spec),\n});\n\nawait stream.send(\"Create a login form\");\n\nconsole.log(stream.spec);\nconsole.log(stream.isStreaming);\n```\n\n### createChatUI\n\n```typescript\nconst chat = createChatUI({ api: \"/api/chat-ui\" });\nawait chat.send(\"Build a settings panel\");\nconsole.log(chat.messages, chat.isStreaming);\n```\n\n## Schema Export\n\nUse `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/vue/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/vue\")\n\n# @json-render/vue\n\nVue 3 components, providers, and composables.\n\n## Providers\n\n### StateProvider\n\n```vue\n<StateProvider :initial-state=\"object\" :on-state-change=\"fn\">\n  <!-- children -->\n</StateProvider>\n```\n\n<table>\n  <thead>\n    <tr>\n      <th>Prop</th>\n      <th>Type</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>store</code></td>\n      <td><code>StateStore</code></td>\n      <td>External store (controlled mode). When provided, <code>initialState</code> and <code>onStateChange</code> are ignored.</td>\n    </tr>\n    <tr>\n      <td><code>initialState</code></td>\n      <td><code>Record&lt;string, unknown&gt;</code></td>\n      <td>Initial state model (uncontrolled mode).</td>\n    </tr>\n    <tr>\n      <td><code>onStateChange</code></td>\n      <td><code>{'(changes: Array<{ path: string; value: unknown }>) => void'}</code></td>\n      <td>Callback when state changes (uncontrolled mode). Called once per <code>set</code> or <code>update</code> with all changed entries.</td>\n    </tr>\n  </tbody>\n</table>\n\n#### External Store (Controlled Mode)\n\nPass a `StateStore` to bypass the internal state and wire json-render to any state management library:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/vue\";\n\nconst store = createStateStore({ count: 0 });\n```\n\n```vue\n<StateProvider :store=\"store\">\n  <!-- children -->\n</StateProvider>\n```\n\n```typescript\n// Mutate from anywhere — Vue re-renders automatically:\nstore.set(\"/count\", 1);\n```\n\n### ActionProvider\n\n```vue\n<ActionProvider :handlers=\"Record<string, ActionHandler>\" :navigate=\"fn\">\n  <!-- children -->\n</ActionProvider>\n\n// type ActionHandler = (params: Record<string, unknown>) => void | Promise<void>;\n```\n\n### VisibilityProvider\n\n```vue\n<VisibilityProvider>\n  <!-- children -->\n</VisibilityProvider>\n```\n\n`VisibilityProvider` reads state from the parent `StateProvider` automatically. Conditions in specs use the `VisibilityCondition` format with `$state` paths (e.g. `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`). See [visibility](/docs/visibility) for the full syntax.\n\n### ValidationProvider\n\n```vue\n<ValidationProvider :custom-functions=\"Record<string, ValidationFunction>\">\n  <!-- children -->\n</ValidationProvider>\n\n// type ValidationFunction = (value: unknown, args?: object) => boolean | Promise<boolean>;\n```\n\n## defineRegistry\n\nCreate a type-safe component registry from a catalog. Components receive `props`, `children`, `emit`, `on`, and `loading` with catalog-inferred types.\n\nWhen the catalog declares actions, the `actions` field is required. When the catalog has no actions (e.g. `actions: {}`), the field is optional. When passing stubs, any `async () => {}` is sufficient.\n\n```typescript\nimport { h } from \"vue\";\nimport { defineRegistry } from \"@json-render/vue\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [h(\"h3\", null, props.title), children]),\n    Button: ({ props, emit }) =>\n      h(\"button\", { onClick: () => emit(\"press\") }, props.label),\n  },\n  // Required when catalog declares actions:\n  actions: {\n    submit: async (params) => { /* ... */ },\n  },\n});\n\n// Pass to <Renderer>\n// <Renderer :spec=\"spec\" :registry=\"registry\" />\n```\n\n## Components\n\n### Renderer\n\n```vue\n<Renderer\n  :spec=\"Spec\"           // The UI spec to render\n  :registry=\"Registry\"   // Component registry (from defineRegistry)\n  :loading=\"boolean\"     // Optional loading state\n  :fallback=\"Component\"  // Optional fallback for unknown types\n/>\n```\n\n### Component Props (via defineRegistry)\n\n```typescript\nimport type { VNode } from \"vue\";\n\ninterface ComponentContext<P> {\n  props: P;                          // Typed props from catalog\n  children?: VNode | VNode[];        // Rendered children (for container components)\n  emit: (event: string) => void;     // Emit a named event (always defined)\n  on: (event: string) => EventHandle; // Get event handle with metadata\n  loading?: boolean;\n  bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions\n}\n\ninterface EventHandle {\n  emit: () => void;              // Fire the event\n  shouldPreventDefault: boolean; // Whether any binding requested preventDefault\n  bound: boolean;                // Whether any handler is bound\n}\n```\n\nUse `emit(\"press\")` for simple event firing. Use `on(\"click\")` when you need metadata like `shouldPreventDefault`:\n\n```typescript\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return h(\"a\", {\n    href: props.href,\n    onClick: (e: MouseEvent) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    },\n  }, props.label);\n},\n```\n\n### BaseComponentProps\n\nCatalog-agnostic base type for building reusable component libraries that are not tied to a specific catalog:\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/vue\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>\n  h(\"div\", null, [props.title, children]);\n```\n\n## Composables\n\n### useStateStore\n\n```typescript\nconst {\n  state,   // ShallowRef<StateModel> — access with state.value\n  get,     // (path: string) => unknown\n  set,     // (path: string, value: unknown) => void\n  update,  // (updates: Record<string, unknown>) => void\n} = useStateStore();\n```\n\n> **Note:** `state` is a `ShallowRef<StateModel>`, not a plain object. Use `state.value` to read the current state. This differs from the React renderer.\n\n### useStateValue\n\n```typescript\nconst value = useStateValue(path: string); // ComputedRef<T | undefined>\n```\n\nReturns a `ComputedRef` that automatically updates when the state at `path` changes. Use `.value` to access the current value.\n\n### useStateBinding (deprecated)\n\n> **Deprecated.** Use `$bindState` expressions with `bindings` prop instead.\n\n```typescript\nconst [value, setValue] = useStateBinding(path: string);\n// value: ComputedRef<T | undefined>\n// setValue: (value: T) => void\n```\n\n### useActions\n\n```typescript\nconst { execute } = useActions();\n// execute(binding: ActionBinding) => Promise<void>\n```\n\n### useAction\n\n```typescript\nconst { execute, isLoading } = useAction(binding: ActionBinding);\n// execute: () => Promise<void>\n// isLoading: ComputedRef<boolean>\n```\n\n### useIsVisible\n\n```typescript\nconst isVisible = useIsVisible(condition?: VisibilityCondition);\n```\n\n### useFieldValidation\n\n```typescript\nconst {\n  state,     // ComputedRef<FieldValidationState>\n  validate,  // () => ValidationResult\n  touch,     // () => void\n  clear,     // () => void\n  errors,    // ComputedRef<string[]>\n  isValid,   // ComputedRef<boolean>\n} = useFieldValidation(path: string, config?: ValidationConfig);\n```\n\n`ValidationConfig` is `{ checks?: ValidationCheck[], validateOn?: 'change' | 'blur' | 'submit' }`.\n\n## Differences from `@json-render/react`\n\n<table>\n  <thead>\n    <tr>\n      <th>API</th>\n      <th>React</th>\n      <th>Vue</th>\n      <th>Note</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>useStateStore().state</code></td>\n      <td><code>StateModel</code> (plain object)</td>\n      <td><code>ShallowRef&lt;StateModel&gt;</code></td>\n      <td>Vue reactivity; use <code>state.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useStateValue()</code></td>\n      <td><code>T | undefined</code></td>\n      <td><code>ComputedRef&lt;T | undefined&gt;</code></td>\n      <td>Vue reactivity; use <code>.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useStateBinding()</code></td>\n      <td><code>[T | undefined, setter]</code></td>\n      <td><code>[ComputedRef&lt;T | undefined&gt;, setter]</code></td>\n      <td>Vue reactivity; use <code>value.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useAction().isLoading</code></td>\n      <td><code>boolean</code></td>\n      <td><code>ComputedRef&lt;boolean&gt;</code></td>\n      <td>Vue reactivity; use <code>.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useFieldValidation().state</code></td>\n      <td><code>FieldValidationState</code></td>\n      <td><code>ComputedRef&lt;FieldValidationState&gt;</code></td>\n      <td>Vue reactivity; use <code>.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useFieldValidation().errors</code></td>\n      <td><code>string[]</code></td>\n      <td><code>ComputedRef&lt;string[]&gt;</code></td>\n      <td>Vue reactivity; use <code>.value</code></td>\n    </tr>\n    <tr>\n      <td><code>useFieldValidation().isValid</code></td>\n      <td><code>boolean</code></td>\n      <td><code>ComputedRef&lt;boolean&gt;</code></td>\n      <td>Vue reactivity; use <code>.value</code></td>\n    </tr>\n    <tr>\n      <td><code>VisibilityContextValue.ctx</code></td>\n      <td><code>CoreVisibilityContext</code></td>\n      <td><code>ComputedRef&lt;CoreVisibilityContext&gt;</code></td>\n      <td>Vue reactivity; use <code>ctx.value</code></td>\n    </tr>\n    <tr>\n      <td><code>children</code> type</td>\n      <td><code>React.ReactNode</code></td>\n      <td><code>VNode | VNode[]</code></td>\n      <td>Platform-specific</td>\n    </tr>\n    <tr>\n      <td><code>useBoundProp</code></td>\n      <td>exported</td>\n      <td>exported</td>\n      <td>Same API; returns <code>[value, setValue]</code></td>\n    </tr>\n    <tr>\n      <td><code>VisibilityProviderProps</code></td>\n      <td>exported</td>\n      <td>not exported (no props)</td>\n      <td>Vue uses slot, no prop needed</td>\n    </tr>\n    <tr>\n      <td>Streaming hooks</td>\n      <td><code>useUIStream</code>, <code>useChatUI</code></td>\n      <td><code>useUIStream</code>, <code>useChatUI</code></td>\n      <td>Same API; returns Vue <code>Ref</code> values</td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/xstate/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/xstate\")\n\n# @json-render/xstate\n\n[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.\n\nRequires `@xstate/store` v3+.\n\n## Installation\n\n```bash\nnpm install @json-render/xstate @json-render/core @json-render/react @xstate/store\n```\n\n## xstateStoreStateStore\n\nCreate a `StateStore` backed by an `@xstate/store` atom.\n\n```typescript\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\n```\n\n### Options\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Required</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>atom</code></td>\n      <td><code>{'Atom<StateModel>'}</code></td>\n      <td>Yes</td>\n      <td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Example\n\n```typescript\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst uiAtom = createAtom({ count: 0 });\nconst store = xstateStoreStateStore({ atom: uiAtom });\n```\n\n```tsx\n<StateProvider store={store}>\n  {/* json-render reads/writes go through @xstate/store */}\n</StateProvider>\n```\n\n## Re-exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Source</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>StateStore</code></td>\n      <td><code>@json-render/core</code></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/yaml/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/yaml\")\n\n# @json-render/yaml\n\nYAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.\n\n## Prompt Generation\n\n### yamlPrompt\n\nGenerate a YAML-format system prompt from any json-render catalog. Works with catalogs from any renderer.\n\n```typescript\nfunction yamlPrompt(\n  catalog: Catalog,\n  options?: YamlPromptOptions\n): string\n```\n\n```typescript\nimport { yamlPrompt } from \"@json-render/yaml\";\n\nconst systemPrompt = yamlPrompt(catalog, {\n  mode: \"standalone\",\n  customRules: [\"Always use dark theme\"],\n  editModes: [\"merge\"],\n});\n```\n\n### YamlPromptOptions\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Default</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>system</code></td>\n      <td><code>string</code></td>\n      <td><code>{'\\\"You are a UI generator that outputs YAML.\\\"'}</code></td>\n      <td>Custom system message intro</td>\n    </tr>\n    <tr>\n      <td><code>mode</code></td>\n      <td><code>{'\\\"standalone\\\" | \\\"inline\\\"'}</code></td>\n      <td><code>{'\\\"standalone\\\"'}</code></td>\n      <td>Standalone outputs only YAML; inline allows conversational responses with embedded YAML fences</td>\n    </tr>\n    <tr>\n      <td><code>customRules</code></td>\n      <td><code>{'string[]'}</code></td>\n      <td><code>{'[]'}</code></td>\n      <td>Additional rules appended to the prompt</td>\n    </tr>\n    <tr>\n      <td><code>editModes</code></td>\n      <td><code>{'EditMode[]'}</code></td>\n      <td><code>{'[\\\"merge\\\"]'}</code></td>\n      <td>Edit modes to document in the prompt (patch, merge, diff)</td>\n    </tr>\n  </tbody>\n</table>\n\n## AI SDK Transform\n\n### createYamlTransform\n\nCreates a `TransformStream` that intercepts AI SDK stream chunks and converts YAML spec/edit blocks into json-render patch data parts.\n\n```typescript\nfunction createYamlTransform(\n  options?: YamlTransformOptions\n): TransformStream<StreamChunk, StreamChunk>\n```\n\nRecognized fence types:\n\n- <code>{'```yaml-spec'}</code> -- Full YAML spec, parsed progressively\n- <code>{'```yaml-edit'}</code> -- Partial YAML, deep-merged with current spec\n- <code>{'```yaml-patch'}</code> -- RFC 6902 JSON Patch lines\n- <code>{'```diff'}</code> -- Unified diff against serialized spec\n\n### pipeYamlRender\n\nConvenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.\n\n```typescript\nfunction pipeYamlRender<T>(\n  stream: ReadableStream<T>,\n  options?: YamlTransformOptions\n): ReadableStream<T>\n```\n\n```typescript\nimport { pipeYamlRender } from \"@json-render/yaml\";\nimport { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeYamlRender(result.toUIMessageStream()));\n  },\n});\nreturn createUIMessageStreamResponse({ stream });\n```\n\n## Streaming Parser\n\n### createYamlStreamCompiler\n\nCreate a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations by diffing each successful parse against the previous snapshot.\n\n```typescript\nfunction createYamlStreamCompiler<T>(\n  initial?: Partial<T>\n): YamlStreamCompiler<T>\n```\n\n```typescript\nimport { createYamlStreamCompiler } from \"@json-render/yaml\";\n\nconst compiler = createYamlStreamCompiler<Spec>();\n\ncompiler.push(\"root: main\\n\");\ncompiler.push(\"elements:\\n  main:\\n    type: Card\\n\");\n\nconst { result, newPatches } = compiler.flush();\n```\n\n### YamlStreamCompiler\n\n<table>\n  <thead>\n    <tr>\n      <th>Method</th>\n      <th>Returns</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>push(chunk)</code></td>\n      <td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>\n      <td>Push a chunk of text, returns current result and new patches</td>\n    </tr>\n    <tr>\n      <td><code>flush()</code></td>\n      <td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>\n      <td>Flush remaining buffer, return final result</td>\n    </tr>\n    <tr>\n      <td><code>getResult()</code></td>\n      <td><code>T</code></td>\n      <td>Get the current compiled result</td>\n    </tr>\n    <tr>\n      <td><code>getPatches()</code></td>\n      <td><code>{'JsonPatch[]'}</code></td>\n      <td>Get all patches applied so far</td>\n    </tr>\n    <tr>\n      <td><code>reset(initial?)</code></td>\n      <td><code>void</code></td>\n      <td>Reset to initial state</td>\n    </tr>\n  </tbody>\n</table>\n\n## Fence Constants\n\nExported string constants for fence detection in custom parsers:\n\n<table>\n  <thead>\n    <tr>\n      <th>Constant</th>\n      <th>Value</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>YAML_SPEC_FENCE</code></td>\n      <td><code>{'\\\"```yaml-spec\\\"'}</code></td>\n    </tr>\n    <tr>\n      <td><code>YAML_EDIT_FENCE</code></td>\n      <td><code>{'\\\"```yaml-edit\\\"'}</code></td>\n    </tr>\n    <tr>\n      <td><code>YAML_PATCH_FENCE</code></td>\n      <td><code>{'\\\"```yaml-patch\\\"'}</code></td>\n    </tr>\n    <tr>\n      <td><code>DIFF_FENCE</code></td>\n      <td><code>{'\\\"```diff\\\"'}</code></td>\n    </tr>\n    <tr>\n      <td><code>FENCE_CLOSE</code></td>\n      <td><code>{'\\\"```\\\"'}</code></td>\n    </tr>\n  </tbody>\n</table>\n\n## Re-exports from @json-render/core\n\n### diffToPatches\n\nGenerate RFC 6902 JSON Patch operations that transform one object into another.\n\n```typescript\nfunction diffToPatches(\n  oldObj: Record<string, unknown>,\n  newObj: Record<string, unknown>,\n  basePath?: string\n): JsonPatch[]\n```\n\n### deepMergeSpec\n\nDeep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse.\n\n```typescript\nfunction deepMergeSpec(\n  base: Record<string, unknown>,\n  patch: Record<string, unknown>\n): Record<string, unknown>\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/api/zustand/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/api/zustand\")\n\n# @json-render/zustand\n\nZustand adapter for json-render's `StateStore` interface.\n\nRequires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.\n\n## Installation\n\n```bash\nnpm install @json-render/zustand @json-render/core @json-render/react zustand\n```\n\n## zustandStateStore\n\nCreate a `StateStore` backed by a Zustand vanilla store.\n\n```typescript\nimport { zustandStateStore } from \"@json-render/zustand\";\n```\n\n### Options\n\n<table>\n  <thead>\n    <tr>\n      <th>Option</th>\n      <th>Type</th>\n      <th>Required</th>\n      <th>Description</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>store</code></td>\n      <td><code>{'StoreApi<S>'}</code></td>\n      <td>Yes</td>\n      <td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>\n    </tr>\n    <tr>\n      <td><code>selector</code></td>\n      <td><code>{'(state: S) => StateModel'}</code></td>\n      <td>No</td>\n      <td>Select the json-render slice from the store state. Defaults to the entire state.</td>\n    </tr>\n    <tr>\n      <td><code>updater</code></td>\n      <td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>\n      <td>No</td>\n      <td>Apply a state change back to the store. Defaults to a shallow merge.</td>\n    </tr>\n  </tbody>\n</table>\n\n### Example\n\n```typescript\nimport { createStore } from \"zustand/vanilla\";\nimport { zustandStateStore } from \"@json-render/zustand\";\nimport { StateProvider } from \"@json-render/react\";\n\nconst bearStore = createStore(() => ({\n  count: 0,\n  name: \"Bear\",\n}));\n\nconst store = zustandStateStore({ store: bearStore });\n```\n\n```tsx\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Zustand */}\n</StateProvider>\n```\n\n### Nested Slice\n\n```typescript\nconst appStore = createStore(() => ({\n  ui: { count: 0 },\n  auth: { token: null },\n}));\n\nconst store = zustandStateStore({\n  store: appStore,\n  selector: (s) => s.ui,\n  updater: (next, s) => s.setState({ ui: next }),\n});\n```\n\n## Re-exports\n\n<table>\n  <thead>\n    <tr>\n      <th>Export</th>\n      <th>Source</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>StateStore</code></td>\n      <td><code>@json-render/core</code></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/catalog/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/catalog\")\n\n# Catalog\n\nThe catalog defines what AI can generate. It's your guardrail.\n\n## What is a Catalog?\n\nA catalog is the vocabulary for your UI. While the [schema](/docs/schemas) defines the grammar (how specs are structured), the catalog defines the vocabulary (what components and actions are available). It lists:\n\n- **Components** — UI elements AI can create (with props and optional slots)\n- **Actions** — Operations AI can trigger\n- **Functions** — Custom validation or transformation functions\n\n## Creating a Catalog\n\n`defineCatalog` is from `@json-render/core`. The `schema` import comes from your platform package (`@json-render/react` or `@json-render/react-native`) and defines the element structure the catalog targets. The catalog definition itself is framework-agnostic.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'\nimport { z } from 'zod';\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    // Define each component with its props schema\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n        padding: z.enum(['sm', 'md', 'lg']).nullable(),\n      }),\n      slots: [\"default\"], // Can contain other components\n      description: \"Container card for grouping content\",\n    },\n    \n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.union([z.string(), z.number()]),\n        format: z.enum(['currency', 'percent', 'number']),\n      }),\n      description: \"Display a single metric value\",\n    },\n  },\n  \n  actions: {\n    submit_form: {\n      params: z.object({\n        formId: z.string(),\n      }),\n      description: 'Submit a form',\n    },\n    \n    export_data: {\n      params: z.object({\n        format: z.enum(['csv', 'pdf', 'json']),\n      }),\n      description: 'Export data in various formats',\n    },\n  },\n});\n```\n\n## Component Definition\n\nEach component in the catalog has:\n\n```typescript\n{\n  props: z.object({...}),  // Zod schema for props (use .nullable() for optional)\n  slots?: string[],        // Named slots for children (e.g., [\"default\"])\n  description?: string,    // Help AI understand when to use it\n}\n```\n\nUse `slots: [\"default\"]` for components that can contain children. The slot name corresponds to where child elements are rendered.\n\n## Generating AI Prompts\n\nUse the `catalog.prompt()` method to generate a system prompt for AI:\n\n```typescript\n// Generate a system prompt from your catalog\nconst systemPrompt = catalog.prompt();\n\n// Or with custom rules for the AI\nconst customPrompt = catalog.prompt({\n  customRules: [\n    \"Always use Card as the root element for forms\",\n    \"Group related inputs in a Stack with direction=vertical\",\n  ],\n});\n\n// Pass this to your AI model as the system prompt\n```\n\n## Next\n\nLearn how to [register components](/docs/registry) in your registry.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/changelog/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/changelog\")\n\n# Changelog\n\nNotable changes and updates to json-render.\n\n## v0.10.0\n\nFebruary 2026\n\n### New: `@json-render/vue`\n\nVue 3 renderer for json-render with full feature parity with `@json-render/react`. Data binding, visibility conditions, actions, validation, repeat scopes, streaming, and external store support.\n\n```bash\nnpm install @json-render/core @json-render/vue\n```\n\n```typescript\nimport { h } from \"vue\";\nimport { defineRegistry, Renderer } from \"@json-render/vue\";\nimport { schema } from \"@json-render/vue/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [h(\"h3\", null, props.title), children]),\n    Button: ({ props, emit }) =>\n      h(\"button\", { onClick: () => emit(\"press\") }, props.label),\n  },\n});\n```\n\nProviders: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`. Composables: `useStateStore`, `useStateValue`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`, `useBoundProp`, `useUIStream`, `useChatUI`.\n\nSee the [Vue API reference](/docs/api/vue) for details.\n\n### New: `@json-render/xstate`\n\n[XState Store](https://stately.ai/docs/xstate-store) (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend for any renderer.\n\n```bash\nnpm install @json-render/xstate @xstate/store\n```\n\n```typescript\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\n\nconst atom = createAtom({ count: 0 });\nconst store = xstateStoreStateStore({ atom });\n```\n\nRequires `@xstate/store` v3+.\n\n### New: `$computed` and `$template` Expressions\n\nTwo new prop expression types for dynamic values:\n\n- **`$template`** -- interpolate state values into strings: `{ \"$template\": \"Hello, ${/user/name}!\" }`\n- **`$computed`** -- call registered functions: `{ \"$computed\": \"fullName\", \"args\": { \"first\": { \"$state\": \"/form/firstName\" } } }`\n\nRegister functions via the `functions` prop on `JSONUIProvider` or `createRenderer`. See [Computed Values](/docs/computed-values) for details.\n\n### New: State Watchers\n\nElements can declare a `watch` field to trigger actions when state values change. Useful for cascading dependencies like country/city selects.\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}\n```\n\n`watch` is a top-level field on elements (sibling of type/props/children), not inside props. Watchers only fire on value changes, not on initial render. See [Watchers](/docs/watchers) for details.\n\n### New: Cross-Field Validation\n\nNew built-in validation functions for cross-field comparisons:\n\n- `equalTo` -- alias for `matches` with clearer semantics\n- `lessThan` -- value must be less than another field\n- `greaterThan` -- value must be greater than another field\n- `requiredIf` -- required only when a condition field is truthy\n\nValidation check args now resolve through `resolvePropValue`, so `$state` expressions work consistently.\n\n### New: `validateForm` Action\n\nBuilt-in action (React) that validates all registered form fields at once and writes `{ valid, errors }` to state:\n\n```json\n{\n  \"on\": {\n    \"press\": [\n      { \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } },\n      { \"action\": \"submitForm\" }\n    ]\n  }\n}\n```\n\n### Improved: shadcn/ui Validation\n\nAll form components now support `checks` and `validateOn` props:\n- Checkbox, Radio, Switch added validation support\n- `validateOn` controls timing: `\"change\"` (default for Select, Checkbox, Radio, Switch), `\"blur\"` (default for Input, Textarea), or `\"submit\"`\n\n### New Examples\n\n- **Vue example** -- standalone Vue 3 app with custom components\n- **Vite Renderers** -- side-by-side React and Vue renderers with shared catalog\n\n---\n\n## v0.9.1\n\nFebruary 2026\n\n### Fixed: Install failure due to private dependency\n\n`@json-render/react`, `@json-render/react-pdf`, and `@json-render/react-native` v0.9.0 failed to install because `@internal/react-state` (a private workspace package) was published as a dependency. The internal package is now bundled into each renderer at build time, so it no longer needs to be resolved from npm.\n\n---\n\n## v0.9.0\n\nFebruary 2026\n\n### New: External State Store\n\nThe `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n\n- Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n- `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop\n- When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n- When `store` is omitted, everything works exactly as before (fully backward compatible)\n- Applied across all platform packages: react, react-native, react-pdf\n- Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\nNew adapter packages: `@json-render/redux`, `@json-render/zustand`, `@json-render/jotai`.\n\nSee the [Data Binding](/docs/data-binding#external-store-controlled-mode) guide for usage.\n\n### Changed: `onStateChange` signature updated (breaking)\n\nThe `onStateChange` callback now receives a single array of changed entries instead of being called once per path. This makes batch updates via `update()` easier to handle:\n\n```ts\n// Before\nonStateChange?: (path: string, value: unknown) => void\n\n// After\nonStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n```\n\nThe callback is only called when a `set()` or `update()` call actually changes the state. A `set()` call produces a single-element array; an `update()` call produces one array with all changed paths.\n\n### Fixed: Server-safe schema import\n\n`@json-render/react` barrel-imports React contexts that call `createContext`, which crashes in Next.js App Router API routes (RSC runtime strips `createContext`). All docs, examples, and skills now import `schema` from `@json-render/react/schema` instead of `@json-render/react`.\n\nFor combined imports, split into separate `schema` (subpath) and client API (main entry) lines:\n\n```ts\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry, Renderer } from \"@json-render/react\";\n```\n\n### Fixed: Chaining actions\n\nFixed an issue where chaining multiple actions on the same event (e.g. `setState` followed by a custom action) did not execute all actions. Affected `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`.\n\n### Fixed: Zod array inner type resolution\n\nFixed safely resolving the inner type for Zod arrays in schema introspection, preventing errors when catalog component props use `z.array()`.\n\n---\n\n## v0.8.0\n\nFebruary 2026\n\n### New: `@json-render/react-pdf`\n\nPDF renderer for json-render, powered by [`@react-pdf/renderer`](https://react-pdf.org/). Define catalogs and registries the same way as `@json-render/react`, but output PDF documents instead of web UI.\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\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\nServer-side rendering APIs:\n\n- `renderToBuffer(spec)` -- render to an in-memory PDF buffer\n- `renderToStream(spec)` -- render to a readable stream (pipe to HTTP response)\n- `renderToFile(spec, path)` -- render directly to a file\n\n15 standard components covering document structure (Document, Page), layout (View, Row, Column), content (Heading, Text, Image, Link), data (Table, List), decorative (Divider, Spacer), and page-level (PageNumber).\n\nSupports custom catalogs with `defineRegistry`, server-safe imports via `@json-render/react-pdf/server`, and full context support (state, visibility, actions, validation, repeat scopes).\n\n---\n\n## v0.7.0\n\nFebruary 2026\n\n### New: `@json-render/shadcn`\n\nPre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.\n\n```bash\nnpm install @json-render/shadcn\n```\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Button: shadcnComponentDefinitions.Button,\n    Input: shadcnComponentDefinitions.Input,\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Button: shadcnComponents.Button,\n    Input: shadcnComponents.Input,\n  },\n});\n```\n\nComponents include: layout (Card, Stack, Grid, Separator), navigation (Tabs, Accordion, Collapsible, Pagination), overlay (Dialog, Drawer, Tooltip, Popover, DropdownMenu), content (Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table), feedback (Progress, Skeleton, Spinner), and input (Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup).\n\nSee the [API reference](/docs/api/shadcn) for full details.\n\n### New: Event Handles (`on()`)\n\nComponents now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:\n\n- `emit()` -- fire the event\n- `shouldPreventDefault` -- whether any action binding requested `preventDefault`\n- `bound` -- whether any handler is bound to this event\n\n```tsx\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### New: `BaseComponentProps`\n\nCatalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.\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### New: Built-in Actions in Schema\n\nSchemas can now declare `builtInActions` -- actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.\n\n### New: `preventDefault` on `ActionBinding`\n\nAction bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.\n\n### Improved: Stream Transform Text Block Splitting\n\n`createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.\n\n### Improved: `defineRegistry` Actions Requirement\n\n`defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.\n\n---\n\n## v0.6.0\n\nFebruary 2026\n\n### New: Chat Mode (Inline GenUI)\n\n> **Note:** These modes were renamed in v0.12.1 — \"Generate\" is now \"Standalone\" and \"Chat\" is now \"Inline\". The old names are accepted as deprecated aliases.\n\njson-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.\n\n```typescript\n// Generate mode (default) — AI outputs only JSONL\nconst prompt = catalog.prompt();\n\n// Chat mode — AI outputs text + JSONL inline\nconst chatPrompt = catalog.prompt({ mode: \"chat\" });\n```\n\nOn the server, `pipeJsonRender()` separates text from JSONL patches in a mixed stream:\n\n```typescript\nimport { pipeJsonRender } from \"@json-render/core\";\nimport { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeJsonRender(result.toUIMessageStream()));\n  },\n});\nreturn createUIMessageStreamResponse({ stream });\n```\n\nOn the client, `useJsonRenderMessage` extracts the spec and text from message parts:\n\n```tsx\nimport { useJsonRenderMessage } from \"@json-render/react\";\n\nfunction ChatMessage({ message }) {\n  const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);\n  return (\n    <div>\n      {text && <Markdown>{text}</Markdown>}\n      {hasSpec && <Renderer spec={spec} registry={registry} />}\n    </div>\n  );\n}\n```\n\n### New: AI SDK Integration\n\nFirst-class Vercel AI SDK support with typed data parts and stream utilities.\n\n- `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n- `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n- `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n- `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n### New: Two-Way Binding\n\nProps can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state without custom `valuePath` props.\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": { \"label\": \"Email\", \"value\": { \"$bindState\": \"/form/email\" } }\n}\n```\n\n```tsx\nimport { useBoundProp } from \"@json-render/react\";\n\nInput: ({ props, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);\n  return <input value={value ?? \"\"} onChange={(e) => setValue(e.target.value)} />;\n}\n```\n\n### New: Expression-Based Props and Visibility\n\nAll dynamic expressions now use structured `$state`, `$item`, and `$index` objects instead of string token rewriting. This is simpler, more explicit, and works for both props and visibility conditions.\n\n**Props:**\n\n```json\n{ \"title\": { \"$state\": \"/user/name\" } }\n{ \"label\": { \"$item\": \"title\" } }\n{ \"position\": { \"$index\": true } }\n```\n\n**Visibility:**\n\n```json\n{ \"$state\": \"/isAdmin\" }\n{ \"$state\": \"/role\", \"eq\": \"admin\" }\n[{ \"$state\": \"/isAdmin\" }, { \"$state\": \"/feature\" }]\n{ \"$or\": [{ \"$state\": \"/roleA\" }, { \"$state\": \"/roleB\" }] }\n{ \"$item\": \"isActive\" }\n{ \"$index\": true, \"gt\": 0 }\n```\n\nComparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`.\n\n### New: React Chat Hooks\n\n- `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n- `useJsonRenderMessage()` — extract spec + text from a message's parts array\n- `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n- `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem`\n\n### New: Chat Example\n\nFull-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming inline UI generation.\n\n### Improved: Renderer Performance\n\n- `ElementRenderer` is now `React.memo`'d for better performance with repeat lists\n- `emit` is always defined (never `undefined`)\n- Repeat scope passes the actual item object, eliminating string token rewriting\n\n### Improved: Utilities\n\n- `applySpecPatch()` — typed wrapper for applying a single patch to a Spec\n- `nestedToFlat()` — convert nested tree specs to flat format\n- `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n### Breaking Changes\n\n- `{ $path }` and `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }` in props\n- Visibility: `{ path }` -> `{ $state }`, `{ and/or/not }` -> `{ $and/$or }` with `not` as operator flag\n- `DynamicValue`: `{ path: string }` -> `{ $state: string }`\n- `repeat.path` -> `repeat.statePath`\n- Action params: `path` -> `statePath` in setState action\n- `actionHandlers` -> `handlers` on `JSONUIProvider` / `ActionProvider`\n- `AuthState` and `{ auth }` visibility conditions removed (model auth as regular state)\n- Legacy catalog API removed: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`\n- React exports removed: `createRendererFromCatalog`, `rewriteRepeatTokens`\n- Codegen: `traverseTree` -> `traverseSpec`\n\nSee the [Migration Guide](/docs/migration) for detailed upgrade instructions.\n\n---\n\n## v0.5.0\n\nFebruary 2026\n\n### New: @json-render/react-native\n\nFull React Native renderer with 25+ standard components, data binding, visibility, actions, and dynamic props. Build AI-generated native mobile UIs with the same catalog-driven approach as web.\n\n```tsx\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 } from \"@json-render/react-native\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...standardComponentDefinitions },\n  actions: standardActionDefinitions,\n});\n\nconst { registry } = defineRegistry(catalog, { components: {} });\n\n<Renderer spec={spec} registry={registry} />\n```\n\nIncludes standard components for layout (Container, Row, Column, ScrollContainer, SafeArea, Pressable, Spacer, Divider), content (Heading, Paragraph, Label, Image, Avatar, Badge, Chip), input (Button, TextInput, Switch, Checkbox, Slider, SearchBar), feedback (Spinner, ProgressBar), and composite (Card, ListItem, Modal).\n\n### New: Event System\n\nComponents now use `emit` to fire named events instead of directly dispatching actions. The element's `on` field maps events to action bindings, decoupling component logic from action handling.\n\n```tsx\n// Component emits a named event\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n),\n\n// Element spec maps events to actions\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\", \"params\": { \"formId\": \"main\" } } }\n}\n```\n\n### New: Repeat/List Rendering\n\nElements can now iterate over state arrays using the `repeat` field. Child elements use `{ \"$item\": \"field\" }` to read from the current item and `{ \"$index\": true }` for the current array index.\n\n```json\n{\n  \"type\": \"Column\",\n  \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" },\n  \"children\": [\"post-card\"]\n}\n```\n\n```json\n{\n  \"type\": \"Card\",\n  \"props\": { \"title\": { \"$item\": \"title\" } }\n}\n```\n\n### New: 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 (patch-only mode)\nbuildUserPrompt({ prompt: \"add a toggle\", currentSpec: spec });\n\n// With runtime state\nbuildUserPrompt({ prompt: \"show data\", state: { todos: [] } });\n```\n\n### New: 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);\nconst fixed = autoFixSpec(spec);\n```\n\n### Improved: State Management\n\n`DataProvider` has been renamed to `StateProvider` with a clearer API. State is now a first-class part of specs. Elements can bind to state via `$state` expressions, and the built-in `setState` action updates state directly.\n\n### Improved: AI Prompts\n\nSchema prompts now include streaming best practices, repeat/list examples, and state patching guidance. Schemas can also define `defaultRules` that are always included in generated prompts.\n\n### Improved: Documentation\n\n- All documentation pages migrated to MDX\n- AI-powered documentation chat\n- Dynamic Open Graph images for all docs pages\n- Improved playground\n\n### Breaking Changes\n\n- `DataProvider` renamed to `StateProvider`\n- `useData` renamed to `useStateStore`, `useDataValue` to `useStateValue`, `useDataBinding` to `useStateBinding`\n- `onAction` renamed to `emit` in component context\n- `DataModel` type renamed to `StateModel`\n- `Action` type renamed to `ActionBinding` (old name still available but deprecated)\n\n---\n\n## v0.4.0\n\nFebruary 2026\n\n### New: Custom Schema System\n\nCreate custom output formats with `defineSchema`. Each renderer now defines its own schema, enabling completely different spec formats for different use cases.\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nconst mySchema = defineSchema((s) => ({\n  spec: s.object({\n    pages: s.array(s.object({\n      title: s.string(),\n      blocks: s.array(s.ref(\"catalog.blocks\")),\n    })),\n  }),\n  catalog: s.object({\n    blocks: s.map({ props: s.zod(), description: s.string() }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate,\n});\n```\n\n### New: Component Slots\n\nComponents can now define which slots they accept. Use `[\"default\"]` for regular children, or named slots like `[\"header\", \"footer\"]` for more complex layouts.\n\n```typescript\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],  // accepts children\n      description: \"A card container\",\n    },\n    Layout: {\n      props: z.object({}),\n      slots: [\"header\", \"content\", \"footer\"],  // named slots\n      description: \"Page layout with header, content, footer\",\n    },\n  },\n});\n```\n\n### New: AI Prompt Generation\n\nCatalogs now generate AI system prompts automatically with `catalog.prompt()`. The prompt includes all component definitions, props schemas, and action descriptions - ensuring the AI only generates valid specs.\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\n\nconst catalog = defineCatalog(schema, {\n  components: { /* ... */ },\n  actions: { /* ... */ },\n});\n\n// Generate system prompt for AI\nconst systemPrompt = catalog.prompt();\n\n// Use with any AI SDK\nconst result = await streamText({\n  model: \"claude-haiku-4.5\",\n  system: systemPrompt,\n  prompt: userMessage,\n});\n```\n\n### New: @json-render/remotion\n\nGenerate AI-powered videos with Remotion. Define video catalogs, stream timeline specs, and render with the Remotion Player.\n\n```tsx\nimport { Player } from \"@remotion/player\";\nimport { Renderer, schema, standardComponentDefinitions } from \"@json-render/remotion\";\n\nconst catalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n  transitions: standardTransitionDefinitions,\n});\n\n<Player\n  component={Renderer}\n  inputProps={{ spec }}\n  durationInFrames={spec.composition.durationInFrames}\n  fps={spec.composition.fps}\n  compositionWidth={spec.composition.width}\n  compositionHeight={spec.composition.height}\n/>\n```\n\nIncludes 10 standard video components (TitleCard, TypingText, SplitScreen, etc.), 7 transition types, and the ClipWrapper utility for custom components.\n\n### New: SpecStream\n\nSpecStream is json-render's streaming format for progressively building specs from JSONL patches. The new compiler API makes it easy to process streaming AI responses.\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);\nsetSpec(result); // Update UI with partial result\n```\n\n### Improved: Dashboard Example\n\nThe dashboard example is now a full-featured accounting dashboard with:\n\n- Persistent SQLite database with Drizzle ORM\n- RESTful API for customers, invoices, expenses, accounts\n- Draggable widget reordering\n- AI-powered widget generation with streaming\n- Real data binding to database records\n\n### Improved: Documentation\n\n- Interactive playground for testing specs\n- New guides: Custom Schema, Streaming, Code Export\n- Full API reference for all packages\n- Integration guides: A2UI, AG-UI, Adaptive Cards, OpenAPI\n\n### Breaking Changes\n\n- `UITree` type renamed to `Spec`\n- Schema is now imported from renderer packages (`@json-render/react`) not core\n- `defineCatalog` now requires a schema as first argument\n\n---\n\n## v0.3.0\n\nJanuary 2026\n\nInternal release with codegen foundations.\n\n- Added `@json-render/codegen` package (spec traversal and JSX serialization)\n- Configurable AI model via environment variables\n- Documentation improvements and bug fixes\n\n*Note: Only @json-render/core was published to npm for this release.*\n\n---\n\n## v0.2.0\n\nJanuary 2026\n\nInitial public release.\n\n- Core catalog and spec types\n- React renderer with contexts for data, actions, visibility\n- AI prompt generation from catalogs\n- Basic streaming support\n- Dashboard example application\n"
  },
  {
    "path": "apps/web/app/(main)/docs/code-export/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/code-export\")\n\n# Code Export\n\nExport generated UI as standalone code for your framework.\n\n## Overview\n\nWhile json-render is designed for dynamic rendering, you can export generated UI as static code. The code generation is intentionally project-specific so you have full control over:\n\n- Component templates (standalone, no json-render dependencies)\n- Package.json and project structure\n- Framework-specific patterns (Next.js, Remix, etc.)\n- How data is passed to components\n\n## Architecture\n\nCode export is split into two parts:\n\n### 1. @json-render/codegen (utilities)\n\nFramework-agnostic utilities for building code generators:\n\n```typescript\nimport {\n  traverseSpec,          // Walk the UI spec\n  collectUsedComponents, // Get all component types used\n  collectStatePaths,      // Get all data binding paths\n  collectActions,        // Get all action names\n  serializeProps,        // Convert props to JSX string\n} from '@json-render/codegen';\n```\n\n### 2. Your Project (generator)\n\nCustom code generator specific to your project and framework:\n\n```typescript\n// lib/codegen/generator.ts\nimport { collectUsedComponents, serializeProps } from '@json-render/codegen';\n\nexport function generateNextJSProject(spec: Spec): GeneratedFile[] {\n  const components = collectUsedComponents(spec);\n  \n  return [\n    { path: 'package.json', content: '...' },\n    { path: 'app/page.tsx', content: '...' },\n    // ... component files\n  ];\n}\n```\n\n## Example: Next.js Export\n\nSee the dashboard example for a complete implementation that exports:\n\n- `package.json` - Dependencies and scripts\n- `tsconfig.json` - TypeScript config\n- `next.config.js` - Next.js config\n- `app/layout.tsx` - Root layout\n- `app/globals.css` - Global styles\n- `app/page.tsx` - Generated page with data\n- `components/ui/*.tsx` - Standalone components\n\n## Standalone Components\n\nThe exported components are standalone with no json-render dependencies. They receive data as props instead of using hooks:\n\n```tsx\n// Generated component (standalone)\ninterface MetricProps {\n  label: string;\n  statePath: string;\n  data?: Record<string, unknown>;\n}\n\nexport function Metric({ label, statePath, data }: MetricProps) {\n  const value = data ? getByPath(data, statePath) : undefined;\n  return (\n    <div>\n      <span>{label}</span>\n      <span>{formatValue(value)}</span>\n    </div>\n  );\n}\n```\n\n## Using the Utilities\n\n### traverseSpec\n\n```typescript\nimport { traverseSpec } from '@json-render/codegen';\n\ntraverseSpec(spec, (element, key, depth, parent) => {\n  console.log(' '.repeat(depth * 2) + `${key}: ${element.type}`);\n});\n```\n\n### collectUsedComponents\n\n```typescript\nimport { collectUsedComponents } from '@json-render/codegen';\n\nconst components = collectUsedComponents(spec);\n// Set { 'Card', 'Metric', 'Chart', 'Table' }\n\n// Generate only the needed component files\nfor (const component of components) {\n  files.push({\n    path: `components/ui/${component.toLowerCase()}.tsx`,\n    content: componentTemplates[component],\n  });\n}\n```\n\n### serializeProps\n\n```typescript\nimport { serializeProps } from '@json-render/codegen';\n\nconst propsStr = serializeProps({\n  title: 'Dashboard',\n  columns: 3,\n  disabled: true,\n});\n// 'title=\"Dashboard\" columns={3} disabled'\n```\n\n## Try It\n\nRun the dashboard example and click \"Export Project\" to see code generation in action:\n\n```bash\ncd examples/dashboard\npnpm dev\n# Open http://dashboard-demo.json-render.localhost:1355\n# Generate a widget, then click \"Export Project\"\n```\n"
  },
  {
    "path": "apps/web/app/(main)/docs/computed-values/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/computed-values\")\n\n# Computed Values\n\nDerive dynamic prop values using registered functions or string templates.\n\n## `$template` — String Interpolation\n\nUse `{ \"$template\": \"...\" }` to embed state values into a string. References use `${/path}` syntax where the path is a JSON Pointer:\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"text\": { \"$template\": \"Hello, ${/user/name}! You have ${/inbox/count} messages.\" }\n  },\n  \"children\": []\n}\n```\n\nIf state is `{ \"user\": { \"name\": \"Alice\" }, \"inbox\": { \"count\": 3 } }`, the text renders as \"Hello, Alice! You have 3 messages.\"\n\nMissing paths resolve to an empty string.\n\n## `$computed` — Registered Functions\n\nUse `{ \"$computed\": \"<name>\", \"args\": { ... } }` to call a named function registered in your catalog. Each arg can be a literal value or any prop expression (`$state`, `$item`, `$cond`, etc.):\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"text\": {\n      \"$computed\": \"fullName\",\n      \"args\": {\n        \"first\": { \"$state\": \"/form/firstName\" },\n        \"last\": { \"$state\": \"/form/lastName\" }\n      }\n    }\n  },\n  \"children\": []\n}\n```\n\n### Registering Functions\n\nFunctions are registered in the catalog and provided at runtime.\n\n**Catalog definition (for AI prompt generation):**\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\n\nconst catalog = defineCatalog(schema, {\n  components: { /* ... */ },\n  functions: {\n    fullName: {\n      description: 'Combines first and last name into a full name',\n    },\n    formatCurrency: {\n      description: 'Formats a number as currency',\n    },\n  },\n});\n```\n\n**Runtime implementation:**\n\n```tsx\nimport { JSONUIProvider } from '@json-render/react';\n\nconst functions = {\n  fullName: (args) => `${args.first ?? ''} ${args.last ?? ''}`.trim(),\n  formatCurrency: (args) => {\n    const value = Number(args.value ?? 0);\n    return new Intl.NumberFormat('en-US', {\n      style: 'currency',\n      currency: (args.currency as string) ?? 'USD',\n    }).format(value);\n  },\n};\n\n<JSONUIProvider registry={registry} functions={functions}>\n  <Renderer spec={spec} registry={registry} />\n</JSONUIProvider>\n```\n\n### Using with `createRenderer`\n\n```tsx\nconst MyRenderer = createRenderer(catalog, components);\n\n<MyRenderer\n  spec={spec}\n  functions={functions}\n/>\n```\n\n## Combining Expressions\n\n`$computed` args can use any expression type. This example computes a total from repeat item fields:\n\n```json\n{\n  \"$computed\": \"lineTotal\",\n  \"args\": {\n    \"price\": { \"$item\": \"price\" },\n    \"quantity\": { \"$item\": \"quantity\" }\n  }\n}\n```\n\n## Next\n\n- [Watchers](/docs/watchers) — react to state changes with cascading actions\n- [Data Binding](/docs/data-binding) — all expression types\n- [Validation](/docs/validation) — validate form inputs\n"
  },
  {
    "path": "apps/web/app/(main)/docs/custom-schema/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/custom-schema\")\n\n# Custom Schema & Renderer\n\nBuild your own schema and renderer with `@json-render/core`.\n\n## Overview\n\n`@json-render/core` is schema-agnostic. While `@json-render/react` provides a ready-to-use schema and renderer, you can create your own to match any JSON structure - whether it's a domain-specific format, an existing protocol, or something entirely custom.\n\n## 1. Define Your Schema\n\nStart by defining the JSON structure your system will use. Here's an example of a simple dashboard schema:\n\n```json\n{\n  \"layout\": \"grid\",\n  \"columns\": 2,\n  \"widgets\": [\n    {\n      \"type\": \"metric\",\n      \"title\": \"Revenue\",\n      \"value\": \"$12,345\",\n      \"trend\": \"up\"\n    },\n    {\n      \"type\": \"chart\",\n      \"title\": \"Sales\",\n      \"chartType\": \"line\",\n      \"dataKey\": \"salesData\"\n    },\n    {\n      \"type\": \"table\",\n      \"title\": \"Recent Orders\",\n      \"columns\": [\"id\", \"customer\", \"amount\"],\n      \"dataKey\": \"orders\"\n    }\n  ]\n}\n```\n\n## 2. Create the Catalog\n\nDefine a catalog that describes your components and validates props using `defineCatalog` — see [Catalog](/docs/catalog).\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { z } from 'zod';\n\nexport const dashboardCatalog = defineCatalog(mySchema, {\n  components: {\n    metric: {\n      description: 'Displays a single metric value',\n      props: z.object({\n        title: z.string(),\n        value: z.string(),\n        trend: z.enum(['up', 'down', 'flat']).optional(),\n        change: z.string().optional(),\n      }),\n    },\n    chart: {\n      description: 'Renders a chart visualization',\n      props: z.object({\n        title: z.string(),\n        chartType: z.enum(['line', 'bar', 'pie', 'area']),\n        dataKey: z.string(),\n        height: z.number().optional(),\n      }),\n    },\n    table: {\n      description: 'Displays tabular data',\n      props: z.object({\n        title: z.string(),\n        columns: z.array(z.string()),\n        dataKey: z.string(),\n        pageSize: z.number().optional(),\n      }),\n    },\n    text: {\n      description: 'Displays text content',\n      props: z.object({\n        content: z.string(),\n        variant: z.enum(['heading', 'body', 'caption']).optional(),\n      }),\n    },\n  },\n});\n```\n\n## 3. Define the Root Schema\n\nCreate a schema for the overall document structure:\n\n```typescript\nimport { z } from 'zod';\n\nconst WidgetSchema = z.object({\n  type: z.string(),\n  title: z.string().optional(),\n  // Additional props validated by catalog\n}).passthrough();\n\nexport const DashboardSchema = z.object({\n  layout: z.enum(['grid', 'stack', 'tabs']),\n  columns: z.number().optional(),\n  widgets: z.array(WidgetSchema),\n});\n\nexport type Dashboard = z.infer<typeof DashboardSchema>;\nexport type Widget = z.infer<typeof WidgetSchema>;\n```\n\n## 4. Build the Renderer\n\nCreate a renderer that maps your schema to React components:\n\n```tsx\nimport React from 'react';\nimport { dashboardCatalog } from './catalog';\nimport type { Dashboard, Widget } from './schema';\n\n// Widget component registry\nconst widgetComponents: Record<string, React.FC<any>> = {\n  metric: ({ title, value, trend, change }) => (\n    <div className=\"p-4 rounded-lg border\">\n      <p className=\"text-sm text-muted-foreground\">{title}</p>\n      <p className=\"text-2xl font-bold\">{value}</p>\n      {trend && (\n        <p className={`text-sm ${trend === 'up' ? 'text-green-500' : 'text-red-500'}`}>\n          {trend === 'up' ? '+' : '-'}{change}\n        </p>\n      )}\n    </div>\n  ),\n\n  chart: ({ title, chartType, data }) => (\n    <div className=\"p-4 rounded-lg border\">\n      <p className=\"font-medium mb-2\">{title}</p>\n      <div className=\"h-48 bg-muted rounded flex items-center justify-center\">\n        {/* Your chart library here */}\n        <span className=\"text-muted-foreground\">{chartType} chart</span>\n      </div>\n    </div>\n  ),\n\n  table: ({ title, columns, data }) => (\n    <div className=\"p-4 rounded-lg border\">\n      <p className=\"font-medium mb-2\">{title}</p>\n      <table className=\"w-full text-sm\">\n        <thead>\n          <tr>\n            {columns.map((col: string) => (\n              <th key={col} className=\"text-left p-2 border-b\">{col}</th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {data?.map((row: any, i: number) => (\n            <tr key={i}>\n              {columns.map((col: string) => (\n                <td key={col} className=\"p-2 border-b\">{row[col]}</td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  ),\n\n  text: ({ content, variant = 'body' }) => {\n    const className = {\n      heading: 'text-xl font-bold',\n      body: 'text-base',\n      caption: 'text-sm text-muted-foreground',\n    }[variant];\n    return <p className={className}>{content}</p>;\n  },\n};\n\n// Main renderer\nexport function DashboardRenderer({\n  spec,\n  data = {},\n}: {\n  spec: Dashboard;\n  data?: Record<string, any>;\n}) {\n  const layoutClass = {\n    grid: `grid gap-4 ${spec.columns ? `grid-cols-${spec.columns}` : 'grid-cols-2'}`,\n    stack: 'flex flex-col gap-4',\n    tabs: 'space-y-4',\n  }[spec.layout];\n\n  return (\n    <div className={layoutClass}>\n      {spec.widgets.map((widget, index) => {\n        const Component = widgetComponents[widget.type];\n        if (!Component) {\n          console.warn(`Unknown widget type: ${widget.type}`);\n          return null;\n        }\n\n        // Resolve data references\n        const widgetData = widget.dataKey ? data[widget.dataKey] : undefined;\n\n        return (\n          <Component\n            key={index}\n            {...widget}\n            data={widgetData}\n          />\n        );\n      })}\n    </div>\n  );\n}\n```\n\n## 5. Generate LLM Prompts\n\nUse the catalog to generate system prompts for AI:\n\n```typescript\nconst systemPrompt = dashboardCatalog.prompt({\n  customRules: [\n    'Use metric widgets for single KPI values',\n    'Use chart widgets for time-series data',\n    'Use table widgets for lists of records',\n    'Limit dashboards to 6 widgets maximum',\n  ],\n});\n\n// Use with any LLM\nconst response = await generateText({\n  model: 'gpt-4',\n  system: systemPrompt,\n  prompt: 'Create a sales dashboard with revenue, orders, and a chart',\n});\n```\n\n## 6. Validate Specs\n\nValidate incoming specs against your schema. Use `catalog.validate()` to check AI output against the catalog's Zod schema:\n\n```typescript\nfunction validateDashboard(spec: unknown) {\n  // Validate root structure\n  const rootResult = DashboardSchema.safeParse(spec);\n  if (!rootResult.success) {\n    return { valid: false, errors: rootResult.error.errors };\n  }\n\n  // Validate each widget's props against the catalog\n  const result = dashboardCatalog.validate(spec);\n  if (!result.success) {\n    return { valid: false, errors: result.error.errors };\n  }\n\n  return { valid: true, errors: [] };\n}\n```\n\n## Usage Example\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\nimport { DashboardRenderer } from './renderer';\nimport type { Dashboard } from './schema';\n\nconst initialSpec: Dashboard = {\n  layout: 'grid',\n  columns: 2,\n  widgets: [\n    { type: 'metric', title: 'Revenue', value: '$12,345', trend: 'up' },\n    { type: 'metric', title: 'Orders', value: '156', trend: 'up' },\n    { type: 'chart', title: 'Sales Trend', chartType: 'line', dataKey: 'sales' },\n    { type: 'table', title: 'Recent Orders', columns: ['id', 'customer', 'amount'], dataKey: 'orders' },\n  ],\n};\n\nconst data = {\n  sales: [/* chart data */],\n  orders: [\n    { id: '001', customer: 'Acme Inc', amount: '$500' },\n    { id: '002', customer: 'Globex', amount: '$750' },\n  ],\n};\n\nexport function MyDashboard() {\n  const [spec, setSpec] = useState(initialSpec);\n\n  return <DashboardRenderer spec={spec} data={data} />;\n}\n```\n\n## Next\n\nSee how to integrate with [A2UI](/docs/a2ui) or [Adaptive Cards](/docs/adaptive-cards) protocols.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/data-binding/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/data-binding\")\n\n# Data Binding\n\nConnect UI elements to dynamic data using expressions in your JSON specs.\n\n## State Model\n\nEvery spec can include a `state` object that holds the data your UI reads from:\n\n```json\n{\n  \"root\": \"greeting\",\n  \"elements\": {\n    \"greeting\": {\n      \"type\": \"Text\",\n      \"props\": { \"content\": { \"$state\": \"/user/name\" } },\n      \"children\": []\n    }\n  },\n  \"state\": {\n    \"user\": { \"name\": \"Alice\" }\n  }\n}\n```\n\nState can also be provided programmatically at runtime. In `@json-render/react`, this is done via `StateProvider` and hooks like `useStateStore`. See the [React API reference](/docs/api/react) for details.\n\n## JSON Pointer Paths\n\nAll paths in json-render follow JSON Pointer (RFC 6901). A path is a string of `/`-separated tokens starting from the root:\n\n```\nGiven this state:\n{\n  \"user\": { \"name\": \"Alice\", \"email\": \"alice@example.com\" },\n  \"todos\": [\n    { \"title\": \"Buy milk\", \"done\": false },\n    { \"title\": \"Walk dog\", \"done\": true }\n  ]\n}\n\n\"/user/name\"      -> \"Alice\"\n\"/user/email\"     -> \"alice@example.com\"\n\"/todos/0/title\"  -> \"Buy milk\"\n\"/todos/1/done\"   -> true\n```\n\n## Expressions\n\nExpressions are special objects you place in props to read dynamic values instead of hardcoding them. There are six expression types.\n\n### `$state` — Read from state\n\nUse `{ \"$state\": \"/path\" }` in any prop to read a value from the state model:\n\n```json\n{\n  \"type\": \"Card\",\n  \"props\": {\n    \"title\": { \"$state\": \"/user/name\" },\n    \"subtitle\": { \"$state\": \"/user/email\" }\n  },\n  \"children\": []\n}\n```\n\nIf state contains `{ \"user\": { \"name\": \"Alice\", \"email\": \"alice@example.com\" } }`, the Card renders with title \"Alice\" and subtitle \"alice@example.com\".\n\n### `$item` — Read from the current repeat item\n\nUse `{ \"$item\": \"field\" }` inside a [repeat](#repeat) to read a field from the current array item:\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": { \"content\": { \"$item\": \"title\" } },\n  \"children\": []\n}\n```\n\nUse `{ \"$item\": \"\" }` to get the entire item object.\n\n### `$index` — Current repeat index\n\nUse `{ \"$index\": true }` inside a [repeat](#repeat) to get the current array index (zero-based number):\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": { \"content\": { \"$index\": true } },\n  \"children\": []\n}\n```\n\n## Repeat\n\nThe `repeat` field on an element renders its children once per item in a state array. It is a top-level field on the element, sibling of `type`, `props`, and `children` — not inside `props`.\n\n```json\n{\n  \"root\": \"todo-list\",\n  \"elements\": {\n    \"todo-list\": {\n      \"type\": \"Column\",\n      \"props\": { \"gap\": 8 },\n      \"repeat\": { \"statePath\": \"/todos\", \"key\": \"id\" },\n      \"children\": [\"todo-item\"]\n    },\n    \"todo-item\": {\n      \"type\": \"Card\",\n      \"props\": {\n        \"title\": { \"$item\": \"title\" },\n        \"subtitle\": { \"$item\": \"description\" }\n      },\n      \"children\": []\n    }\n  },\n  \"state\": {\n    \"todos\": [\n      { \"id\": \"1\", \"title\": \"Buy milk\", \"description\": \"2% or whole\" },\n      { \"id\": \"2\", \"title\": \"Walk dog\", \"description\": \"Around the park\" }\n    ]\n  }\n}\n```\n\n- `repeat.statePath` — JSON Pointer to the state array\n- `repeat.key` — field name on each item to use as a stable key for rendering\n\nInside `todo-item`, `{ \"$item\": \"title\" }` reads the `title` field from whichever array item is currently being rendered. `{ \"$index\": true }` would return `0` for the first item, `1` for the second, and so on.\n\n## Two-Way Binding with `$bindState`\n\nForm components use `{ \"$bindState\": \"/path\" }` on their natural value prop for two-way binding. The component reads from and writes to the state path.\n\n### Value prop (text inputs)\n\n```json\n{\n  \"type\": \"TextInput\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Enter your email\"\n  },\n  \"children\": []\n}\n```\n\n### Checked prop (switches, checkboxes)\n\n```json\n{\n  \"type\": \"Switch\",\n  \"props\": {\n    \"label\": \"Enable notifications\",\n    \"checked\": { \"$bindState\": \"/settings/notifications\" }\n  },\n  \"children\": []\n}\n```\n\n### Pressed prop (toggle buttons)\n\n```json\n{\n  \"type\": \"ToggleButton\",\n  \"props\": {\n    \"label\": \"Bold\",\n    \"pressed\": { \"$bindState\": \"/editor/bold\" }\n  },\n  \"children\": []\n}\n```\n\n## Two-Way Binding with `$bindItem`\n\nInside a repeat scope, use `{ \"$bindItem\": \"field\" }` to bind to a field on the current item:\n\n```json\n{\n  \"type\": \"Switch\",\n  \"props\": {\n    \"label\": \"Done\",\n    \"checked\": { \"$bindItem\": \"completed\" }\n  },\n  \"children\": []\n}\n```\n\nUse `{ \"$bindItem\": \"\" }` to bind to the entire item.\n\n`statePath` is not used for component binding. It remains for `repeat.statePath` (array iteration path) and action params like `setState.statePath` (target path for mutations).\n\n## Conditional Props\n\nUse `$cond` / `$then` / `$else` to pick a prop value based on a condition:\n\n```json\n{\n  \"type\": \"Badge\",\n  \"props\": {\n    \"label\": {\n      \"$cond\": { \"$state\": \"/user/isAdmin\" },\n      \"$then\": \"Admin\",\n      \"$else\": \"Member\"\n    }\n  },\n  \"children\": []\n}\n```\n\nThe condition uses the same [visibility](/docs/visibility) expression format.\n\n## Template Strings\n\nUse `{ \"$template\": \"...\" }` to interpolate state values into a string using `${/path}` syntax:\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"text\": { \"$template\": \"Welcome back, ${/user/name}!\" }\n  },\n  \"children\": []\n}\n```\n\nSee [Computed Values](/docs/computed-values) for details on `$template` and `$computed` expressions.\n\n## Quick Reference\n\n<div className=\"my-6 overflow-x-auto\">\n  <table className=\"mdx-table w-full text-sm border-collapse\">\n    <thead>\n      <tr>\n        <th>Expression</th>\n        <th>Syntax</th>\n        <th>Context</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td><code>{\"$state\"}</code></td>\n        <td><code>{'{ \"$state\": \"/path\" }'}</code></td>\n        <td>Anywhere</td>\n      </tr>\n      <tr>\n        <td><code>{\"$item\"}</code></td>\n        <td><code>{'{ \"$item\": \"field\" }'}</code></td>\n        <td>Inside repeat only</td>\n      </tr>\n      <tr>\n        <td><code>{\"$index\"}</code></td>\n        <td><code>{'{ \"$index\": true }'}</code></td>\n        <td>Inside repeat only</td>\n      </tr>\n      <tr>\n        <td><code>{\"$cond\"}</code></td>\n        <td><code>{'{ \"$cond\": ..., \"$then\": ..., \"$else\": ... }'}</code></td>\n        <td>Anywhere</td>\n      </tr>\n      <tr>\n        <td><code>{\"$bindState\"}</code></td>\n        <td><code>{'{ \"$bindState\": \"/path\" }'}</code></td>\n        <td>Form components (value, checked, pressed)</td>\n      </tr>\n      <tr>\n        <td><code>{\"$bindItem\"}</code></td>\n        <td><code>{'{ \"$bindItem\": \"field\" }'}</code></td>\n        <td>Form components inside repeat</td>\n      </tr>\n      <tr>\n        <td><code>{\"$template\"}</code></td>\n        <td><code>{'{ \"$template\": \"Hello, ${/name}!\" }'}</code></td>\n        <td>Anywhere (string props)</td>\n      </tr>\n      <tr>\n        <td><code>{\"$computed\"}</code></td>\n        <td><code>{'{ \"$computed\": \"fn\", \"args\": { ... } }'}</code></td>\n        <td>Anywhere (requires registered function)</td>\n      </tr>\n    </tbody>\n  </table>\n</div>\n\n## External Store (Controlled Mode)\n\nFor advanced use cases, you can pass a `StateStore` to `StateProvider` to use your own state management (Redux, Zustand, XState, etc.) instead of the built-in internal store:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\nconst store = createStateStore({ user: { name: \"Alice\" } });\n\n<StateProvider store={store}>\n  {children}\n</StateProvider>\n\n// Mutate from anywhere — React re-renders automatically:\nstore.set(\"/user/name\", \"Bob\");\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored. The store is the single source of truth. See the [React API reference](/docs/api/react#external-store-controlled-mode) for the full `StateStore` interface.\n\n## Next\n\n- [Visibility](/docs/visibility) — conditionally show or hide elements\n- [Action handlers](/docs/registry#action-handlers) — respond to user interactions\n- [React API reference](/docs/api/react) — React-specific hooks for programmatic state access\n"
  },
  {
    "path": "apps/web/app/(main)/docs/generation-modes/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/generation-modes\")\n\n# Generation Modes\n\njson-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.\n\nThe mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.\n\n<GenerationModesDiagram />\n\n## Standalone Mode\n\nIn standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.\n\nThis is the default mode and is ideal for:\n\n- Playground and builder tools\n- Form generators\n- Dashboard builders\n- Any UI where the generated interface is the whole response\n\n### Setup\n\n```typescript\nimport { streamText } from \"ai\";\n\n// Standalone mode is the default (no mode option needed)\nconst systemPrompt = catalog.prompt({\n  customRules: [\n    \"Use Card as root for forms and small UIs.\",\n    \"Use Grid for multi-column layouts.\",\n  ],\n});\n\nconst result = streamText({\n  model: \"anthropic/claude-haiku-4.5\",\n  system: systemPrompt,\n  prompt: userPrompt,\n});\n```\n\n### Client\n\nOn the client, use `useUIStream` from `@json-render/react` or the lower-level `createSpecStreamCompiler` from `@json-render/core` to compile the JSONL stream into a spec:\n\n```tsx\nimport { useUIStream } from \"@json-render/react\";\n\nfunction Playground() {\n  const { spec, isStreaming, send } = useUIStream({\n    api: \"/api/generate\",\n  });\n\n  return (\n    <Renderer\n      spec={spec}\n      registry={registry}\n      loading={isStreaming}\n    />\n  );\n}\n```\n\n### Example output\n\nThe AI outputs only JSONL — one patch per line, no surrounding text:\n\n```\n{\"op\":\"add\",\"path\":\"/root\",\"value\":\"card-1\"}\n{\"op\":\"add\",\"path\":\"/elements/card-1\",\"value\":{\"type\":\"Card\",\"props\":{\"title\":\"Sign In\"},\"children\":[\"email\",\"password\",\"submit\"]}}\n{\"op\":\"add\",\"path\":\"/elements/email\",\"value\":{\"type\":\"Input\",\"props\":{\"label\":\"Email\",\"name\":\"email\",\"type\":\"email\"}}}\n{\"op\":\"add\",\"path\":\"/elements/password\",\"value\":{\"type\":\"Input\",\"props\":{\"label\":\"Password\",\"name\":\"password\",\"type\":\"password\"}}}\n{\"op\":\"add\",\"path\":\"/elements/submit\",\"value\":{\"type\":\"Button\",\"props\":{\"label\":\"Sign In\"}}}\n```\n\n## Inline Mode\n\nIn inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).\n\nThis is ideal for:\n\n- AI chatbots with rich UI responses\n- Copilot experiences\n- Educational assistants\n- Any conversational interface where generated UI is embedded in chat messages\n\n### Setup\n\n```typescript\nimport { streamText } from \"ai\";\nimport { pipeJsonRender } from \"@json-render/core\";\nimport { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n\n// Enable inline mode\nconst systemPrompt = catalog.prompt({ mode: \"inline\" });\n\nconst result = streamText({\n  model: yourModel,\n  system: systemPrompt,\n  messages,\n});\n\n// In your API route, pipe the stream through pipeJsonRender\n// to separate text from JSONL patches\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeJsonRender(result.toUIMessageStream()));\n  },\n});\n\nreturn createUIMessageStreamResponse({ stream });\n```\n\n`pipeJsonRender` inspects each line of the AI's response. Lines that parse as JSONL patches are emitted as `data-spec` parts (which the renderer picks up). Everything else is passed through as text.\n\n### Client\n\nOn the client, use `useJsonRenderMessage` from `@json-render/react` to extract the spec from a chat message's parts:\n\n```tsx\nimport { useChat } from \"@ai-sdk/react\";\nimport { useJsonRenderMessage } from \"@json-render/react\";\n\nfunction Chat() {\n  const { messages, input, handleInputChange, handleSubmit } = useChat();\n\n  return (\n    <div>\n      {messages.map((msg) => (\n        <ChatMessage key={msg.id} message={msg} />\n      ))}\n      {/* input form */}\n    </div>\n  );\n}\n\nfunction ChatMessage({ message }) {\n  const { spec } = useJsonRenderMessage(message.parts);\n\n  return (\n    <div>\n      {/* Render text parts */}\n      {message.parts\n        .filter((p) => p.type === \"text\")\n        .map((p, i) => <p key={i}>{p.text}</p>)}\n\n      {/* Render the generated UI inline */}\n      {spec && (\n        <Renderer\n          spec={spec}\n          registry={registry}\n        />\n      )}\n    </div>\n  );\n}\n```\n\n### Example output\n\nThe AI writes a brief explanation, then JSONL patches on their own lines:\n\n```\nHere's a dashboard showing the latest crypto prices:\n\n{\"op\":\"add\",\"path\":\"/root\",\"value\":\"dashboard\"}\n{\"op\":\"add\",\"path\":\"/state/prices\",\"value\":[{\"name\":\"Bitcoin\",\"price\":98450},{\"name\":\"Ethereum\",\"price\":3120}]}\n{\"op\":\"add\",\"path\":\"/elements/dashboard\",\"value\":{\"type\":\"Grid\",\"props\":{\"columns\":\"2\"},\"children\":[\"btc\",\"eth\"]}}\n{\"op\":\"add\",\"path\":\"/elements/btc\",\"value\":{\"type\":\"Metric\",\"props\":{\"label\":\"Bitcoin\",\"value\":{\"$state\":\"/prices/0/price\"}}}}\n{\"op\":\"add\",\"path\":\"/elements/eth\",\"value\":{\"type\":\"Metric\",\"props\":{\"label\":\"Ethereum\",\"value\":{\"$state\":\"/prices/1/price\"}}}}\n```\n\nIf the user asks a simple question (\"what does BTC stand for?\"), the AI replies with text only — no JSONL.\n\n## Quick Comparison\n\n<div className=\"my-6 overflow-x-auto\">\n  <table className=\"mdx-table w-full text-sm border-collapse\">\n    <thead>\n      <tr>\n        <th />\n        <th>Standalone</th>\n        <th>Inline</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>Output format</td>\n        <td>JSONL only</td>\n        <td>Text + JSONL</td>\n      </tr>\n      <tr>\n        <td>Text-only replies</td>\n        <td>No</td>\n        <td>Yes</td>\n      </tr>\n      <tr>\n        <td>System prompt</td>\n        <td><code>{\"catalog.prompt()\"}</code></td>\n        <td><code>{'catalog.prompt({ mode: \"inline\" })'}</code></td>\n      </tr>\n      <tr>\n        <td>Stream utility</td>\n        <td><code>{\"useUIStream\"}</code></td>\n        <td><code>{\"pipeJsonRender\"}</code>{\" + \"}<code>{\"useJsonRenderMessage\"}</code></td>\n      </tr>\n      <tr>\n        <td>Typical use case</td>\n        <td>Playground, builders</td>\n        <td>Chatbots, copilots</td>\n      </tr>\n    </tbody>\n  </table>\n</div>\n\nBoth modes use the same JSONL patch format (RFC 6902) and the same catalog/registry system. The only difference is whether the AI is allowed to include prose alongside the patches.\n\n## Next\n\n- Learn about the [JSONL streaming format](/docs/streaming)\n- See the [AI SDK integration](/docs/ai-sdk) for setup with the Vercel AI SDK\n"
  },
  {
    "path": "apps/web/app/(main)/docs/installation/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/installation\")\n\n# Installation\n\nInstall the core package plus your renderer of choice.\n\n## For React UI\n\n<PackageInstall packages=\"@json-render/core @json-render/react\" />\n\nPeer dependencies: `react ^19.0.0` and `zod ^4.0.0`.\n\n<PackageInstall packages=\"react zod\" />\n\n## For Vue\n\n<PackageInstall packages=\"@json-render/core @json-render/vue\" />\n\nPeer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.\n\n<PackageInstall packages=\"vue zod\" />\n\n## For Svelte\n\n<PackageInstall packages=\"@json-render/core @json-render/svelte\" />\n\nPeer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.\n\n<PackageInstall packages=\"svelte zod\" />\n\n## For React UI with shadcn/ui\n\nPre-built components for fast prototyping and production use:\n\n<PackageInstall packages=\"@json-render/core @json-render/react @json-render/shadcn\" />\n\nRequires Tailwind CSS in your project. See the [@json-render/shadcn API reference](/docs/api/shadcn) for usage.\n\n## For React Native\n\n<PackageInstall packages=\"@json-render/core @json-render/react-native\" />\n\n## For Remotion Video\n\n<PackageInstall packages=\"@json-render/core @json-render/remotion remotion @remotion/player\" />\n\n## For React Email\n\n<PackageInstall packages=\"@json-render/core @json-render/react-email @react-email/components @react-email/render\" />\n\n## For External State Management (Optional)\n\nIf you want to wire json-render to an existing state management library instead of the built-in store, install the adapter for your library:\n\n<PackageInstall packages=\"@json-render/zustand\" />\n\n<PackageInstall packages=\"@json-render/redux\" />\n\n<PackageInstall packages=\"@json-render/jotai\" />\n\n<PackageInstall packages=\"@json-render/xstate\" />\n\nSee the [Data Binding](/docs/data-binding#external-store-controlled-mode) guide for usage.\n\n## For AI Integration\n\nTo use json-render with AI models, you'll also need the Vercel AI SDK:\n\n<PackageInstall packages=\"ai\" />\n"
  },
  {
    "path": "apps/web/app/(main)/docs/layout.tsx",
    "content": "import { DocsMobileNav } from \"@/components/docs-mobile-nav\";\nimport { DocsSidebar } from \"@/components/docs-sidebar\";\nimport { CopyPageButton } from \"@/components/copy-page-button\";\nimport { TableOfContents } from \"@/components/table-of-contents\";\n\nexport default function DocsLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <>\n      <DocsMobileNav />\n      <div className=\"max-w-7xl mx-auto px-6 py-8 lg:py-12 flex gap-12\">\n        {/* Sidebar */}\n        <aside className=\"w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto\">\n          <DocsSidebar />\n        </aside>\n\n        {/* Content */}\n        <div className=\"flex-1 min-w-0 max-w-2xl pb-20\">\n          <div className=\"flex justify-end mb-4\">\n            <CopyPageButton />\n          </div>\n          <article>{children}</article>\n        </div>\n\n        {/* On this page */}\n        <aside className=\"w-44 shrink-0 hidden xl:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto\">\n          <TableOfContents />\n        </aside>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/(main)/docs/migration/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/migration\")\n\n# Migration Guide\n\nThis guide covers breaking changes introduced in v0.6.0 and how to update your code.\n\n## State Provider\n\n`DataProvider` has been renamed to `StateProvider`, and its props have changed.\n\n**Before:**\n\n```tsx\nimport { DataProvider } from \"@json-render/react\";\n\n<DataProvider data={myData} getValue={getter} setValue={setter}>\n  {children}\n</DataProvider>\n```\n\n**After:**\n\n```tsx\nimport { StateProvider } from \"@json-render/react\";\n\n<StateProvider initialState={myData} onStateChange={(path, value) => console.log(path, value)}>\n  {children}\n</StateProvider>\n```\n\n`StateProvider` now manages state internally. Use `useStateStore()` to access `get`, `set`, and `update`.\n\n<table>\n<thead>\n<tr>\n<th>Before</th>\n<th>After</th>\n</tr>\n</thead>\n<tbody>\n<tr><td><code>DataProvider</code></td><td><code>StateProvider</code></td></tr>\n<tr><td><code>data</code> prop</td><td><code>initialState</code> prop</td></tr>\n<tr><td><code>getValue</code> / <code>setValue</code> props</td><td>Removed (use <code>useStateStore()</code> hook for <code>get</code> / <code>set</code>)</td></tr>\n<tr><td><code>useData</code></td><td><code>useStateStore</code></td></tr>\n<tr><td><code>useDataValue</code></td><td><code>useStateValue</code></td></tr>\n<tr><td><code>useDataBinding</code></td><td><code>useStateBinding</code> (deprecated, use <code>useBoundProp</code> instead)</td></tr>\n<tr><td><code>DataModel</code> type</td><td><code>StateModel</code> type</td></tr>\n</tbody>\n</table>\n\n## Dynamic Expressions\n\nAll dynamic value expressions have been renamed to use `$state`, `$item`, and `$index`.\n\n**Before:**\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"label\": { \"$path\": \"/user/name\" },\n    \"count\": { \"$data\": \"/items/length\" }\n  }\n}\n```\n\n**After:**\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"label\": { \"$state\": \"/user/name\" },\n    \"count\": { \"$state\": \"/items/length\" }\n  }\n}\n```\n\nInside repeat scopes, use `$item` and `$index`:\n\n```json\n{\n  \"type\": \"Card\",\n  \"props\": {\n    \"title\": { \"$item\": \"name\" },\n    \"subtitle\": { \"$index\": true }\n  }\n}\n```\n\n<table>\n<thead>\n<tr>\n<th>Before</th>\n<th>After</th>\n</tr>\n</thead>\n<tbody>\n<tr><td><code>{'{ \"$path\": \"/...\" }'}</code></td><td><code>{'{ \"$state\": \"/...\" }'}</code></td></tr>\n<tr><td><code>{'{ \"$data\": \"/...\" }'}</code></td><td><code>{'{ \"$state\": \"/...\" }'}</code></td></tr>\n</tbody>\n</table>\n\n## Two-Way Binding\n\nForm components no longer use `valuePath` / `statePath` props. Instead, use `$bindState` expressions on the value prop, and `useBoundProp` in your registry.\n\n**Before (catalog):**\n\n```typescript\nInput: {\n  props: z.object({\n    label: z.string(),\n    valuePath: z.string(),\n    placeholder: z.string().optional(),\n  }),\n}\n```\n\n**Before (spec):**\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": { \"label\": \"Email\", \"valuePath\": \"/form/email\" }\n}\n```\n\n**Before (registry):**\n\n```tsx\nInput: ({ props }) => {\n  const [value, setValue] = useStateBinding(props.valuePath);\n  return <input value={value ?? \"\"} onChange={(e) => setValue(e.target.value)} />;\n}\n```\n\n**After (catalog):**\n\n```typescript\nInput: {\n  props: z.object({\n    label: z.string(),\n    value: z.string().optional(),\n    placeholder: z.string().optional(),\n  }),\n}\n```\n\n**After (spec):**\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": { \"label\": \"Email\", \"value\": { \"$bindState\": \"/form/email\" } }\n}\n```\n\n**After (registry):**\n\n```tsx\nInput: ({ props, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);\n  return <input value={value ?? \"\"} onChange={(e) => setValue(e.target.value)} />;\n}\n```\n\n`$bindState` reads from and writes to the given state path. Inside repeat scopes, use `$bindItem` to bind to a field on the current item:\n\n```json\n{\n  \"type\": \"Checkbox\",\n  \"props\": { \"checked\": { \"$bindItem\": \"completed\" } }\n}\n```\n\n## Visibility Conditions\n\nVisibility conditions have been renamed to use `$state`, `$and`, and `$or`.\n\n**Before:**\n\n```json\n{ \"path\": \"/isAdmin\" }\n{ \"eq\": [{ \"path\": \"/role\" }, \"admin\"] }\n{ \"and\": [{ \"path\": \"/isAdmin\" }, { \"path\": \"/feature\" }] }\n{ \"or\": [{ \"path\": \"/roleA\" }, { \"path\": \"/roleB\" }] }\n```\n\n**After:**\n\n```json\n{ \"$state\": \"/isAdmin\" }\n{ \"$state\": \"/role\", \"eq\": \"admin\" }\n{ \"$and\": [{ \"$state\": \"/isAdmin\" }, { \"$state\": \"/feature\" }] }\n{ \"$or\": [{ \"$state\": \"/roleA\" }, { \"$state\": \"/roleB\" }] }\n```\n\nYou can also use an array as shorthand for `$and`:\n\n```json\n[{ \"$state\": \"/isAdmin\" }, { \"$state\": \"/feature\" }]\n```\n\nInside repeat scopes, use `$item` and `$index`:\n\n```json\n{ \"$item\": \"isActive\" }\n{ \"$index\": true, \"eq\": 0 }\n```\n\n## Event System\n\nComponents now use `emit` to fire named events. `onAction` has been removed.\n\n**Before:**\n\n```tsx\nButton: ({ props, onAction }) => (\n  <button onClick={() => onAction?.(\"press\")}>{props.label}</button>\n)\n```\n\n**After:**\n\n```tsx\nButton: ({ props, emit }) => (\n  <button onClick={() => emit(\"press\")}>{props.label}</button>\n)\n```\n\n`emit` is always defined (never `undefined`), so optional chaining is not needed.\n\n## Actions Context\n\n`dispatch` has been renamed to `execute`, and the provider prop has been renamed from `actionHandlers` to `handlers`.\n\n**Before:**\n\n```tsx\nconst { dispatch } = useActions();\ndispatch({ action: \"submit\", params: {} });\n\n<ActionProvider actionHandlers={myHandlers}>\n```\n\n**After:**\n\n```tsx\nconst { execute } = useActions();\nexecute({ action: \"submit\", params: {} });\n\n<ActionProvider handlers={myHandlers}>\n```\n\n## Repeat / List Rendering\n\nThe `repeat` field now uses `statePath` instead of `path`.\n\n**Before:**\n\n```json\n{\n  \"type\": \"Column\",\n  \"repeat\": { \"path\": \"/todos\", \"key\": \"id\" },\n  \"children\": [\"todo-item\"]\n}\n```\n\n**After:**\n\n```json\n{\n  \"type\": \"Column\",\n  \"repeat\": { \"statePath\": \"/todos\", \"key\": \"id\" },\n  \"children\": [\"todo-item\"]\n}\n```\n\n## Catalog Creation\n\n`createCatalog` and `generateSystemPrompt` have been replaced by `defineSchema` + `defineCatalog`.\n\n**Before:**\n\n```typescript\nimport { createCatalog, generateSystemPrompt } from \"@json-render/core\";\n\nconst catalog = createCatalog({\n  name: \"my-app\",\n  components: { /* ... */ },\n  actions: { /* ... */ },\n});\n\nconst prompt = generateSystemPrompt(catalog);\n```\n\n**After:**\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\n\nconst catalog = defineCatalog(schema, {\n  components: { /* ... */ },\n  actions: { /* ... */ },\n});\n\nconst prompt = catalog.prompt();\n\n// Inline mode prompt (formerly \"chat\")\nconst inlinePrompt = catalog.prompt({ mode: \"inline\" });\n```\n\n## Generation Modes\n\nThe generation mode values passed to `catalog.prompt()` have been renamed for clarity:\n\n- `\"generate\"` is now `\"standalone\"`\n- `\"chat\"` is now `\"inline\"`\n\nThe old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.\n\n**Before:**\n\n```typescript\nconst prompt = catalog.prompt({ mode: \"generate\" });\nconst chatPrompt = catalog.prompt({ mode: \"chat\" });\n```\n\n**After:**\n\n```typescript\nconst prompt = catalog.prompt({ mode: \"standalone\" });\nconst inlinePrompt = catalog.prompt({ mode: \"inline\" });\n```\n\nThe default mode (when no `mode` option is provided) is `\"standalone\"`, which behaves identically to the previous `\"generate\"` default.\n\n## Validation\n\n`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.\n\n**Before:**\n\n```json\n{ \"fn\": \"required\", \"message\": \"Required\" }\n{ \"fn\": \"minLength\", \"args\": { \"length\": 8 }, \"message\": \"Too short\" }\n```\n\n**After:**\n\n```json\n{ \"type\": \"required\", \"message\": \"Required\" }\n{ \"type\": \"minLength\", \"args\": { \"min\": 8 }, \"message\": \"Too short\" }\n```\n\n<table>\n<thead>\n<tr>\n<th>Before</th>\n<th>After</th>\n</tr>\n</thead>\n<tbody>\n<tr><td><code>{'{ fn: \"required\" }'}</code></td><td><code>{'{ type: \"required\" }'}</code></td></tr>\n<tr><td><code>{'ValidationProvider functions={...}'}</code></td><td><code>{'ValidationProvider customFunctions={...}'}</code></td></tr>\n<tr><td><code>useFieldValidation(path, checks)</code></td><td><code>useFieldValidation(path, config)</code> where config is <code>{'{ checks, validateOn? }'}</code></td></tr>\n</tbody>\n</table>\n\n## Visibility Provider\n\nThe `auth` prop has been removed from `VisibilityProvider`. Auth state should be modeled as regular state.\n\n**Before:**\n\n```tsx\n<VisibilityProvider auth={{ isSignedIn: true, role: \"admin\" }}>\n```\n\n```json\n{ \"auth\": \"signedIn\" }\n```\n\n**After:**\n\n```tsx\n<StateProvider initialState={{ auth: { isSignedIn: true, role: \"admin\" } }}>\n  <VisibilityProvider>\n```\n\n```json\n{ \"$state\": \"/auth/isSignedIn\" }\n```\n\n## Codegen\n\n`traverseTree` has been renamed to `traverseSpec`, `SpecVisitor` to `TreeVisitor`, and the visitor callback now receives a `key` parameter.\n\n**Before:**\n\n```typescript\nimport { traverseTree } from \"@json-render/codegen\";\n\ntraverseTree(tree, (element) => {\n  // ...\n});\n```\n\n**After:**\n\n```typescript\nimport { traverseSpec } from \"@json-render/codegen\";\n\ntraverseSpec(spec, (element, key) => {\n  // ...\n});\n```\n\n## Action Params\n\nAction params in specs now use `statePath` instead of `path`.\n\n**Before:**\n\n```json\n{\n  \"on\": {\n    \"press\": { \"action\": \"setState\", \"params\": { \"path\": \"/count\", \"value\": 0 } }\n  }\n}\n```\n\n**After:**\n\n```json\n{\n  \"on\": {\n    \"press\": { \"action\": \"setState\", \"params\": { \"statePath\": \"/count\", \"value\": 0 } }\n  }\n}\n```\n\n## Removed Exports\n\nThe following exports have been removed from `@json-render/core`:\n\n<table>\n  <thead>\n    <tr>\n      <th>Removed</th>\n      <th>Replacement</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td><code>createCatalog</code></td>\n      <td><code>defineCatalog(schema, config)</code></td>\n    </tr>\n    <tr>\n      <td><code>generateCatalogPrompt</code></td>\n      <td><code>catalog.prompt()</code></td>\n    </tr>\n    <tr>\n      <td><code>generateSystemPrompt</code></td>\n      <td><code>catalog.prompt()</code></td>\n    </tr>\n    <tr>\n      <td><code>ComponentDefinition</code></td>\n      <td>Use catalog component config directly</td>\n    </tr>\n    <tr>\n      <td><code>CatalogConfig</code></td>\n      <td>Use <code>defineCatalog</code> parameters</td>\n    </tr>\n    <tr>\n      <td><code>SystemPromptOptions</code></td>\n      <td>Use <code>PromptOptions</code></td>\n    </tr>\n    <tr>\n      <td><code>LogicExpression</code></td>\n      <td>Use <code>VisibilityCondition</code></td>\n    </tr>\n    <tr>\n      <td><code>AuthState</code></td>\n      <td>Model auth as regular state (e.g. <code>/auth/isSignedIn</code>)</td>\n    </tr>\n    <tr>\n      <td><code>evaluateLogicExpression</code></td>\n      <td>Use <code>evaluateVisibility</code></td>\n    </tr>\n    <tr>\n      <td><code>createRendererFromCatalog</code></td>\n      <td>Use <code>defineRegistry</code></td>\n    </tr>\n    <tr>\n      <td><code>traverseTree</code> (codegen)</td>\n      <td>Use <code>traverseSpec</code></td>\n    </tr>\n  </tbody>\n</table>\n"
  },
  {
    "path": "apps/web/app/(main)/docs/openapi/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/openapi\")\n\n# OpenAPI Integration\n\nUse json-render to generate dynamic forms and UIs from [OpenAPI/Swagger](https://swagger.io/specification/) schemas.\n\n<div className=\"rounded-lg border border-amber-500/50 bg-amber-500/10 p-4 mb-8\">\n  <p className=\"text-sm text-amber-700 dark:text-amber-300\">\n    <strong>Concept:</strong> This page demonstrates how json-render can support OpenAPI schemas. The examples are illustrative and may require adaptation for production use.\n  </p>\n</div>\n\n## Why OpenAPI?\n\nOpenAPI specifications describe your API's endpoints, request bodies, and response schemas. By converting OpenAPI schemas to json-render specs, you can:\n\n- Automatically generate forms for API endpoints\n- Display API responses with type-aware rendering\n- Keep your UI in sync with your API schema\n- Let AI generate UIs that match your API contracts\n\n## Example OpenAPI Schema\n\nA typical OpenAPI schema for a request body:\n\n```json\n{\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/users\": {\n      \"post\": {\n        \"summary\": \"Create a new user\",\n        \"operationId\": \"createUser\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CreateUserRequest\"\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"CreateUserRequest\": {\n        \"type\": \"object\",\n        \"required\": [\"email\", \"name\"],\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"User's full name\",\n            \"minLength\": 1,\n            \"maxLength\": 100\n          },\n          \"email\": {\n            \"type\": \"string\",\n            \"format\": \"email\",\n            \"description\": \"User's email address\"\n          },\n          \"age\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"maximum\": 150,\n            \"description\": \"User's age\"\n          },\n          \"role\": {\n            \"type\": \"string\",\n            \"enum\": [\"admin\", \"user\", \"guest\"],\n            \"default\": \"user\",\n            \"description\": \"User's role\"\n          },\n          \"preferences\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"newsletter\": {\n                \"type\": \"boolean\",\n                \"default\": false\n              },\n              \"theme\": {\n                \"type\": \"string\",\n                \"enum\": [\"light\", \"dark\", \"system\"]\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n## Define an OpenAPI-to-UI Catalog\n\nCreate components that map to OpenAPI data types:\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\nexport const openapiCatalog = defineCatalog(schema, {\n  components: {\n    Form: {\n      description: 'API form container',\n      props: z.object({\n        operationId: z.string(),\n        endpoint: z.string(),\n        method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),\n        title: z.string().optional(),\n        description: z.string().optional(),\n      }),\n    },\n    StringField: {\n      description: 'String input field',\n      props: z.object({\n        name: z.string(),\n        label: z.string(),\n        description: z.string().optional(),\n        required: z.boolean().optional(),\n        format: z.enum(['text', 'email', 'uri', 'uuid', 'date', 'date-time', 'password']).optional(),\n        minLength: z.number().optional(),\n        maxLength: z.number().optional(),\n        pattern: z.string().optional(),\n        placeholder: z.string().optional(),\n        defaultValue: z.string().optional(),\n      }),\n    },\n    NumberField: {\n      description: 'Number input field',\n      props: z.object({\n        name: z.string(),\n        label: z.string(),\n        description: z.string().optional(),\n        required: z.boolean().optional(),\n        type: z.enum(['integer', 'number']).optional(),\n        minimum: z.number().optional(),\n        maximum: z.number().optional(),\n        defaultValue: z.number().optional(),\n      }),\n    },\n    BooleanField: {\n      description: 'Boolean toggle field',\n      props: z.object({\n        name: z.string(),\n        label: z.string(),\n        description: z.string().optional(),\n        defaultValue: z.boolean().optional(),\n      }),\n    },\n    EnumField: {\n      description: 'Enum selection field',\n      props: z.object({\n        name: z.string(),\n        label: z.string(),\n        description: z.string().optional(),\n        required: z.boolean().optional(),\n        options: z.array(z.object({\n          value: z.string(),\n          label: z.string().optional(),\n        })),\n        defaultValue: z.string().optional(),\n      }),\n    },\n    ObjectField: {\n      description: 'Nested object group',\n      props: z.object({\n        name: z.string(),\n        label: z.string(),\n        description: z.string().optional(),\n        collapsible: z.boolean().optional(),\n      }),\n    },\n  },\n  actions: {\n    submit: {\n      description: 'Submit form to API endpoint',\n      params: z.object({ operationId: z.string() }),\n    },\n    reset: {\n      description: 'Reset form to defaults',\n      params: z.object({}),\n    },\n  },\n});\n```\n\n## Convert OpenAPI Schema to Spec\n\nTransform OpenAPI schemas into json-render specs by recursively walking the schema properties and mapping each type to the corresponding catalog component. The converter handles nested objects, enums, arrays, and all primitive types.\n\n## Usage Example\n\n```tsx\n'use client';\n\nimport { OpenAPIForm } from './openapi-form';\nimport { operationToSpec } from './openapi-to-spec';\n\n// Your OpenAPI schema (typically loaded from your API)\nconst createUserSchema = {\n  type: 'object',\n  required: ['email', 'name'],\n  properties: {\n    name: { type: 'string', description: \"User's full name\" },\n    email: { type: 'string', format: 'email', description: \"User's email\" },\n    age: { type: 'integer', minimum: 0, maximum: 150 },\n    role: { type: 'string', enum: ['admin', 'user', 'guest'], default: 'user' },\n  },\n};\n\n// Convert to spec\nconst spec = operationToSpec(\n  'createUser',\n  'POST',\n  '/api/users',\n  createUserSchema,\n  'Create User',\n  'Add a new user to the system',\n);\n\nexport function CreateUserForm() {\n  const handleSubmit = async (data: Record<string, unknown>) => {\n    const response = await fetch('/api/users', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(data),\n    });\n\n    if (response.ok) {\n      console.log('User created!');\n    }\n  };\n\n  return <OpenAPIForm spec={spec} onSubmit={handleSubmit} />;\n}\n```\n\n## Auto-generating from OpenAPI Document\n\nLoad and parse an OpenAPI document to generate forms for all operations:\n\n```typescript\nimport SwaggerParser from '@apidevtools/swagger-parser';\nimport { operationToSpec } from './openapi-to-spec';\n\nexport async function loadOpenAPISpecs(specUrl: string) {\n  const api = await SwaggerParser.dereference(specUrl);\n  const specs: Record<string, any> = {};\n\n  for (const [path, methods] of Object.entries(api.paths)) {\n    for (const [method, operation] of Object.entries(methods)) {\n      if (!operation.requestBody?.content?.['application/json']?.schema) continue;\n\n      const schema = operation.requestBody.content['application/json'].schema;\n      const operationId = operation.operationId || `${method}_${path.replace(/\\//g, '_')}`;\n\n      specs[operationId] = operationToSpec(\n        operationId,\n        method,\n        path,\n        schema,\n        operation.summary,\n        operation.description,\n      );\n    }\n  }\n\n  return specs;\n}\n\n// Usage\nconst specs = await loadOpenAPISpecs('https://api.example.com/openapi.json');\n// specs.createUser, specs.updateUser, etc.\n```\n\n## Next\n\nLearn about [streaming](/docs/streaming) for progressive UI rendering.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs\")\n\n# Introduction\n\njson-render is a framework for **Generative UI** — AI-generated interfaces that are safe, predictable, and render natively on any platform.\n\n## What is Generative UI?\n\nMost AI integrations treat the interface as fixed. Developers build layouts ahead of time, and AI fills in the data — a chatbot response, a summary, a recommendation. The UI itself never changes.\n\n**Generative UI is different.** The AI generates the interface itself: which components to show, how to arrange them, what data to bind, what actions to wire up. Every response can produce a unique, purpose-built UI tailored to the user's request.\n\nThe challenge is that unconstrained AI output is unpredictable. It can hallucinate component names, produce invalid structures, or generate unsafe code. You need a way to let AI be creative with layout and composition while keeping it within boundaries you control.\n\nThat is what json-render does. You define a **catalog** of components and actions. AI generates JSON constrained to that catalog. Your components render the result natively — on web or mobile — with full type safety and no arbitrary code execution.\n\n## How json-render Works\n\n### 1. Define your catalog\n\nA catalog declares what AI can use: components with typed props, actions with typed params.\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string() }),\n      slots: [\"default\"],\n    },\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n      }),\n    },\n  },\n});\n```\n\n### 2. AI generates a spec\n\nGiven a prompt like \"show me a revenue dashboard\", AI outputs a JSON spec — a flat tree of elements constrained to your catalog:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Revenue Dashboard\" },\n      \"children\": [\"metric-1\", \"metric-2\"]\n    },\n    \"metric-1\": {\n      \"type\": \"Metric\",\n      \"props\": { \"label\": \"Total Revenue\", \"value\": \"$48,200\" }\n    },\n    \"metric-2\": {\n      \"type\": \"Metric\",\n      \"props\": { \"label\": \"Growth\", \"value\": \"+12%\" }\n    }\n  }\n}\n```\n\n### 3. Your components render it\n\nMap catalog types to real components with a registry, then render the spec:\n\n```tsx\nimport { Renderer, StateProvider, VisibilityProvider } from '@json-render/react';\n\n<StateProvider initialState={{}}>\n  <VisibilityProvider>\n    <Renderer spec={spec} registry={registry} />\n  </VisibilityProvider>\n</StateProvider>\n```\n\nThe result is a native UI built from your own components — not an iframe, not markdown, not generated code. The AI chose the structure; you control everything else.\n\n## Key Concepts\n\n- **[Catalog](/docs/catalog)** — Define the components, actions, and validation functions AI can use. This is the contract between your app and the AI.\n- **[Registry](/docs/registry)** — Map catalog types to platform-specific implementations. React components on web, React Native views on mobile.\n- **[Specs](/docs/specs)** — The JSON output AI generates. A flat tree of typed elements with props, children, data bindings, and visibility conditions.\n- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.\n- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.\n- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.\n- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.\n\n## Next\n\n- [Installation](/docs/installation) — Add json-render to your project\n- [Quick Start](/docs/quick-start) — Build your first generative UI in 5 minutes\n"
  },
  {
    "path": "apps/web/app/(main)/docs/quick-start/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/quick-start\")\n\n# Quick Start\n\nGet up and running with json-render in 5 minutes.\n\n## 1. Define your catalog\n\nCreate a catalog that defines what components AI can use:\n\n```typescript\n// lib/catalog.ts\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Container card with optional title\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string().nullable(),\n      }),\n      description: \"Clickable button that triggers an action\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n      }),\n      description: \"Text paragraph\",\n    },\n  },\n  actions: {\n    submit: {\n      params: z.object({ formId: z.string() }),\n      description: \"Submit a form\",\n    },\n    navigate: {\n      params: z.object({ url: z.string() }),\n      description: \"Navigate to a URL\",\n    },\n  },\n});\n```\n\n## 2. Define your components\n\nUse `defineRegistry` to map catalog types to React components. Each component receives type-safe `props`, `children`, and `emit`:\n\n```tsx\n// lib/registry.tsx\nimport { defineRegistry } from '@json-render/react';\nimport { catalog } from './catalog';\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <div className=\"p-4 border rounded-lg\">\n        <h2 className=\"font-bold\">{props.title}</h2>\n        {props.description && (\n          <p className=\"text-gray-600\">{props.description}</p>\n        )}\n        {children}\n      </div>\n    ),\n    Button: ({ props, emit }) => (\n      <button\n        className=\"px-4 py-2 bg-blue-500 text-white rounded\"\n        onClick={() => emit(\"press\")}\n      >\n        {props.label}\n      </button>\n    ),\n    Text: ({ props }) => (\n      <p>{props.content}</p>\n    ),\n  },\n});\n```\n\n## 3. Create an API route\n\nSet up a streaming API route for AI generation:\n\n```typescript\n// app/api/generate/route.ts\nimport { streamText } from 'ai';\nimport { catalog } from '@/lib/catalog';\n\nexport async function POST(req: Request) {\n  const { prompt } = await req.json();\n\n  // Generate system prompt from catalog\n  const systemPrompt = catalog.prompt();\n\n  const result = streamText({\n    model: 'anthropic/claude-haiku-4.5',\n    system: systemPrompt,\n    prompt,\n  });\n\n  return result.toTextStreamResponse();\n}\n```\n\n## 4. Render the UI\n\nUse providers and the `Renderer` with your registry to display AI-generated UI:\n\n```tsx\n// app/page.tsx\n'use client';\n\nimport { Renderer, StateProvider, ActionProvider, VisibilityProvider, ValidationProvider, useUIStream } from '@json-render/react';\nimport { registry } from '@/lib/registry';\n\nexport default function Page() {\n  const { spec, isStreaming, send } = useUIStream({\n    api: '/api/generate',\n  });\n\n  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    const formData = new FormData(e.currentTarget);\n    send(formData.get('prompt') as string);\n  };\n\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{\n          submit: (params) => console.log('Submit:', params),\n          navigate: (params) => console.log('Navigate:', params),\n        }}>\n          <ValidationProvider customFunctions={{}}>\n            <form onSubmit={handleSubmit}>\n              <input\n                name=\"prompt\"\n                placeholder=\"Describe what you want...\"\n                className=\"border p-2 rounded\"\n              />\n              <button type=\"submit\" disabled={isStreaming}>\n                Generate\n              </button>\n            </form>\n\n            <div className=\"mt-8\">\n              <Renderer spec={spec} registry={registry} loading={isStreaming} />\n            </div>\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Quick Start with shadcn/ui\n\nIf you want to skip defining components from scratch, use `@json-render/shadcn` for 36 pre-built components:\n\n```typescript\n// lib/catalog.ts\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Heading: shadcnComponentDefinitions.Heading,\n    Button: shadcnComponentDefinitions.Button,\n    Input: shadcnComponentDefinitions.Input,\n  },\n  actions: {},\n});\n```\n\n```tsx\n// lib/registry.tsx\nimport { defineRegistry } from '@json-render/react';\nimport { shadcnComponents } from '@json-render/shadcn';\nimport { catalog } from './catalog';\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Heading: shadcnComponents.Heading,\n    Button: shadcnComponents.Button,\n    Input: shadcnComponents.Input,\n  },\n});\n```\n\nSee the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list.\n\n## Next steps\n\n- Learn about [catalogs](/docs/catalog) in depth\n- Explore [data binding](/docs/data-binding) for dynamic values\n- Add [action handlers](/docs/registry#action-handlers) for interactivity\n- Implement [conditional visibility](/docs/visibility)\n- Use [pre-built shadcn/ui components](/docs/api/shadcn) for fast prototyping\n"
  },
  {
    "path": "apps/web/app/(main)/docs/registry/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/registry\")\n\n# Registry\n\nA registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generate — the registry provides the *how*.\n\nWhat a registry contains depends on the schema you use. Each package defines its own schema, which determines the shape of both the catalog and the registry.\n\n- **`@json-render/react`** — Components (React elements) and action handlers\n- **`@json-render/react-native`** — Components (React Native elements) and action handlers\n- **`@json-render/react-email`** — Email components (React Email / HTML)\n- **`@json-render/remotion`** — Clip components, transitions, and effects\n\n## @json-render/react\n\n### defineRegistry\n\nUse `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:\n\n```tsx\nimport { defineRegistry } from '@json-render/react';\nimport { myCatalog } from './catalog';\n\nexport const { registry, handlers, executeAction } = defineRegistry(myCatalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h2>{props.title}</h2>\n        {props.description && <p>{props.description}</p>}\n        {children}\n      </div>\n    ),\n\n    Button: ({ props, emit }) => (\n      <button onClick={() => emit(\"press\")}>\n        {props.label}\n      </button>\n    ),\n  },\n\n  actions: {\n    submit_form: async (params, setState) => {\n      const res = await fetch('/api/submit', {\n        method: 'POST',\n        body: JSON.stringify(params),\n      });\n      const result = await res.json();\n      setState((prev) => ({ ...prev, formResult: result }));\n    },\n\n    export_data: async (params) => {\n      const blob = await generateExport(params.format);\n      downloadBlob(blob, `export.${params.format}`);\n    },\n  },\n});\n```\n\nThe returned object contains:\n\n- `registry` — component registry for `<Renderer />`\n- `handlers` — factory for ActionProvider-compatible handlers\n- `executeAction` — imperative action dispatch (for use outside the React tree)\n\n### Component Props\n\nEach component receives a `ComponentContext` object:\n\n```typescript\ninterface ComponentContext {\n  props: T;                                // Type-safe props from your catalog\n  children?: React.ReactNode;              // Rendered children (for slot components)\n  emit: (event: string) => void;           // Emit a named event (always defined)\n  on: (event: string) => EventHandle;      // Get event handle with metadata\n  loading?: boolean;                       // Whether the renderer is in a loading state\n  bindings?: Record<string, string>;       // State paths from $bindState/$bindItem expressions\n}\n\ninterface EventHandle {\n  emit: () => void;              // Fire the event\n  shouldPreventDefault: boolean; // Whether any binding requested preventDefault\n  bound: boolean;                // Whether any handler is bound\n}\n```\n\nProps are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.\n\nUse `emit(\"press\")` for simple event firing. Use `on(\"click\")` when you need to inspect event metadata:\n\n```tsx\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a\n      href={props.href}\n      onClick={(e) => {\n        if (click.shouldPreventDefault) e.preventDefault();\n        click.emit();\n      }}\n    >\n      {props.label}\n    </a>\n  );\n},\n```\n\n#### Using `bindings` for two-way binding\n\nWhen a spec uses `{ \"$bindState\": \"/path\" }` or `{ \"$bindItem\": \"field\" }` on a prop, the renderer resolves the **value** into `props` and provides the **write-back path** in `bindings`. Use the `useBoundProp` hook to wire both together:\n\n```tsx\nimport { useBoundProp, defineRegistry } from '@json-render/react';\n\n// Inside your registry:\nTextInput: ({ props, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);\n  return (\n    <input\n      value={value ?? \"\"}\n      onChange={(e) => setValue(e.target.value)}\n    />\n  );\n},\n```\n\n`useBoundProp` returns `[resolvedValue, setter]`. The setter writes to the bound state path. If no binding exists (the prop is a literal), the setter is a no-op.\n\n### Action Handlers\n\nInstead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.\n\nActions are declared in your [catalog](/docs/catalog). The `@json-render/react` schema supports an `actions` key where you define what operations AI can trigger:\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema';\nimport { z } from 'zod';\n\nconst catalog = defineCatalog(schema, {\n  components: { /* ... */ },\n  actions: {\n    submit_form: {\n      params: z.object({\n        formId: z.string(),\n      }),\n      description: 'Submit a form',\n    },\n    export_data: {\n      params: z.object({\n        format: z.enum(['csv', 'pdf', 'json']),\n      }),\n    },\n    navigate: {\n      params: z.object({\n        url: z.string(),\n      }),\n    },\n  },\n});\n```\n\nAction handlers receive `(params, setState, state)` and are defined inside `defineRegistry`:\n\n```tsx\nexport const { handlers, executeAction } = defineRegistry(catalog, {\n  actions: {\n    submit_form: async (params, setState) => {\n      const response = await fetch('/api/submit', {\n        method: 'POST',\n        body: JSON.stringify({ formId: params.formId }),\n      });\n      const result = await response.json();\n      setState((prev) => ({ ...prev, formResult: result }));\n    },\n\n    export_data: async (params) => {\n      const blob = await generateExport(params.format);\n      downloadBlob(blob, `export.${params.format}`);\n    },\n\n    navigate: (params) => {\n      window.location.href = params.url;\n    },\n  },\n});\n```\n\n### Data Binding\n\nMost data binding is handled automatically by the renderer — `$state`, `$item`, and `$index` expressions in props are resolved before your component receives them. See the [Data Binding](/docs/data-binding) guide for the full reference.\n\nFor two-way binding (form inputs), use `{ \"$bindState\": \"/path\" }` on the natural value prop (or `{ \"$bindItem\": \"field\" }` inside repeat scopes). The renderer provides a `bindings` map with the state path for each bound prop. Use `useBoundProp` to get `[value, setValue]`:\n\n```tsx\nimport { useBoundProp } from '@json-render/react';\n\n// Inside defineRegistry components:\n\nInput: ({ props, bindings }) => {\n  const [value, setValue] = useBoundProp<string>(\n    props.value,\n    bindings?.value\n  );\n  return (\n    <input\n      value={value ?? ''}\n      onChange={(e) => setValue(e.target.value)}\n      placeholder={props.placeholder}\n    />\n  );\n},\n```\n\nFor read-only state access (e.g. displaying a value from state), use `$state` expressions in props — they are resolved before the component receives them. For custom logic, use `useStateStore` and `getByPath` from `@json-render/core`.\n\n### Using the Renderer\n\nWire everything together with providers and the `<Renderer />` component:\n\n```tsx\nimport { useMemo, useRef } from 'react';\nimport {\n  Renderer,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n} from '@json-render/react';\nimport { registry, handlers } from './registry';\n\nfunction App({ spec, state, setState }) {\n  const stateRef = useRef(state);\n  const setStateRef = useRef(setState);\n  stateRef.current = state;\n  setStateRef.current = setState;\n\n  const actionHandlers = useMemo(\n    () => handlers(() => setStateRef.current, () => stateRef.current),\n    [],\n  );\n\n  return (\n    <StateProvider initialState={state}>\n      <VisibilityProvider>\n        <ActionProvider handlers={actionHandlers}>\n          <Renderer spec={spec} registry={registry} />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## @json-render/react-native\n\n`@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:\n\n```tsx\nimport { defineRegistry } from '@json-render/react-native';\nimport { View, Text, Pressable } from 'react-native';\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <View style={styles.card}>\n        <Text style={styles.title}>{props.title}</Text>\n        {children}\n      </View>\n    ),\n\n    Button: ({ props, emit }) => (\n      <Pressable onPress={() => emit(\"press\")}>\n        <Text>{props.label}</Text>\n      </Pressable>\n    ),\n  },\n});\n```\n\nSee the [@json-render/react-native API reference](/docs/api/react-native) for the full API.\n\n## @json-render/react-email\n\n`@json-render/react-email` uses `defineRegistry` like React and React Native. Components render to React Email primitives (`@react-email/components`). Use `renderToHtml` or `renderToPlainText` for server-side email output:\n\n```tsx\nimport { defineRegistry } from '@json-render/react-email';\nimport { renderToHtml } from '@json-render/react-email';\nimport { Body, Container, Heading, Text } from '@react-email/components';\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <Container style={{ padding: 16, backgroundColor: '#fff' }}>\n        <Heading>{props.title}</Heading>\n        {children}\n      </Container>\n    ),\n  },\n});\n\nconst html = await renderToHtml(spec, { registry });\n```\n\nSee the [@json-render/react-email API reference](/docs/api/react-email) for the full API.\n\n## @json-render/remotion\n\n`@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:\n\n```tsx\nimport { Renderer, standardComponents } from '@json-render/remotion';\n\n// Use the standard components directly\n<Renderer spec={timelineSpec} components={standardComponents} />\n\n// Or extend with your own\nconst components = {\n  ...standardComponents,\n  CustomSlide: ({ clip }) => <AbsoluteFill>{/* ... */}</AbsoluteFill>,\n};\n```\n\nThe Remotion schema also supports `transitions` and `effects` in the catalog rather than actions.\n\nSee the [@json-render/remotion API reference](/docs/api/remotion) for the full API.\n\n## Next\n\nLearn about [data binding](/docs/data-binding) for dynamic values.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/renderers/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\nexport const metadata = pageMetadata(\"docs/renderers\");\n\n# Renderers\n\njson-render supports multiple output targets. Each renderer takes the same core concept -- a JSON spec constrained to a catalog -- and renders it natively on a different platform or into a different format.\n\nAll renderers share the same workflow:\n\n1. Define a catalog with `defineCatalog`\n2. AI generates a JSON spec\n3. The renderer turns the spec into platform-native output\n\n<table>\n  <thead>\n    <tr>\n      <th>Renderer</th>\n      <th>Package</th>\n      <th>Output</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>React</td>\n      <td>\n        <code>@json-render/react</code>\n      </td>\n      <td>React component tree</td>\n    </tr>\n    <tr>\n      <td>Vue</td>\n      <td>\n        <code>@json-render/vue</code>\n      </td>\n      <td>Vue 3 component tree</td>\n    </tr>\n    <tr>\n      <td>Svelte</td>\n      <td>\n        <code>@json-render/svelte</code>\n      </td>\n      <td>Svelte 5 component tree</td>\n    </tr>\n    <tr>\n      <td>Solid</td>\n      <td>\n        <code>@json-render/solid</code>\n      </td>\n      <td>SolidJS component tree</td>\n    </tr>\n    <tr>\n      <td>shadcn/ui</td>\n      <td>\n        <code>@json-render/shadcn</code>\n      </td>\n      <td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>\n    </tr>\n    <tr>\n      <td>React Native</td>\n      <td>\n        <code>@json-render/react-native</code>\n      </td>\n      <td>Native mobile views</td>\n    </tr>\n    <tr>\n      <td>Image</td>\n      <td>\n        <code>@json-render/image</code>\n      </td>\n      <td>SVG / PNG (via Satori)</td>\n    </tr>\n    <tr>\n      <td>React PDF</td>\n      <td>\n        <code>@json-render/react-pdf</code>\n      </td>\n      <td>PDF documents</td>\n    </tr>\n    <tr>\n      <td>Remotion</td>\n      <td>\n        <code>@json-render/remotion</code>\n      </td>\n      <td>Video compositions</td>\n    </tr>\n  </tbody>\n</table>\n\n## React\n\nRender specs as React component trees in the browser. Supports data binding, streaming, actions, validation, visibility, and computed values.\n\n```tsx\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { schema } from \"@json-render/react/schema\";\n\nconst { registry } = defineRegistry(catalog, { components });\n<Renderer spec={spec} registry={registry} />;\n```\n\nUse `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.\n\n## Vue\n\nVue 3 renderer with full feature parity with React: data binding, visibility, actions, validation, repeat scopes, and streaming.\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/vue\";\nimport { schema } from \"@json-render/vue/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [h(\"h3\", null, props.title), children]),\n  },\n});\n```\n\nUses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.\n\n## Svelte\n\nSvelte 5 renderer with runes-compatible context helpers, visibility conditions, actions, and streaming support.\n\n```typescript\nimport { defineRegistry, Renderer } from \"@json-render/svelte\";\nimport { schema } from \"@json-render/svelte/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => /* Svelte snippet */,\n  },\n});\n```\n\nSee the [@json-render/svelte API reference](/docs/api/svelte) for details.\n\n## Solid\n\nSolidJS renderer with fine-grained reactivity, state bindings, validation, visibility, and event-driven actions.\n\n```tsx\nimport { defineRegistry, Renderer } from \"@json-render/solid\";\nimport { schema } from \"@json-render/solid/schema\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: (renderProps) => <div>{renderProps.children}</div>,\n  },\n});\n\n<Renderer spec={spec} registry={registry} />;\n```\n\nSee the [@json-render/solid API reference](/docs/api/solid) for details.\n\n## shadcn/ui\n\n36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.\n\n```tsx\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Button: shadcnComponentDefinitions.Button,\n  },\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Button: shadcnComponents.Button,\n  },\n});\n```\n\nSee the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list.\n\n## React Native\n\nRender specs as native mobile views. Includes 25+ standard components and standard action definitions.\n\n```tsx\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 } from \"@json-render/react-native\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...standardComponentDefinitions },\n  actions: standardActionDefinitions,\n});\n\nconst { registry } = defineRegistry(catalog, { components: {} });\n<Renderer spec={spec} registry={registry} />;\n```\n\nSee the [@json-render/react-native API reference](/docs/api/react-native) for details.\n\n## Image\n\nGenerate SVG and PNG images from JSON specs using Satori. Ideal for OG images, social cards, and banners.\n\n```typescript\nimport { renderToSvg, renderToPng } from \"@json-render/image/render\";\n\nconst svg = await renderToSvg(spec, { fonts });\nconst png = await renderToPng(spec, { fonts });\n```\n\nNine standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer. PNG output requires `@resvg/resvg-js` as an optional peer dependency.\n\nSee the [@json-render/image API reference](/docs/api/image) for details.\n\n## React PDF\n\nGenerate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.\n\n```typescript\nimport {\n  renderToBuffer,\n  renderToStream,\n  renderToFile,\n} from \"@json-render/react-pdf\";\n\nconst buffer = await renderToBuffer(spec);\nconst stream = await renderToStream(spec);\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nStandard components include Document, Page, View, Row, Column, Heading, Text, Image, Table, List, Divider, Spacer, Link, and PageNumber.\n\nSee the [@json-render/react-pdf API reference](/docs/api/react-pdf) for details.\n\n## Remotion\n\nTurn JSON timeline specs into video compositions with Remotion.\n\n```tsx\nimport { Player } from \"@remotion/player\";\nimport { Renderer } from \"@json-render/remotion\";\n\n<Player\n  component={Renderer}\n  inputProps={{ spec }}\n  durationInFrames={spec.composition.durationInFrames}\n  fps={spec.composition.fps}\n  compositionWidth={spec.composition.width}\n  compositionHeight={spec.composition.height}\n/>;\n```\n\nUses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.\n\nSee the [@json-render/remotion API reference](/docs/api/remotion) for details.\n\n## Custom Renderers\n\nYou can build your own renderer for any output target. See the [Custom Schema & Renderer](/docs/custom-schema) guide for how to define a custom schema and wire it to your own rendering logic.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/schemas/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/schemas\")\n\n# Schemas\n\nSchemas define the structure and validation rules for your UI specs.\n\n## What is a Schema?\n\nA schema defines the JSON structure that describes your UI. It includes:\n\n- **Element structure** — How components are nested and referenced\n- **Property types** — What props each component accepts\n- **Data binding syntax** — How to reference dynamic data\n- **Action format** — How user interactions are defined\n\n## Schema-Agnostic by Design\n\njson-render can work with any JSON schema. `@json-render/core` provides the primitives to define catalogs and renderers for any format:\n\n- **@json-render/react** — The built-in flat element tree schema\n- **[A2UI](/docs/a2ui)** — Google's Agent-to-User Interaction protocol\n- **[Adaptive Cards](/docs/adaptive-cards)** — Microsoft's platform-agnostic UI format\n- **AG-UI** — CopilotKit's Agent User Interaction Protocol\n- **OpenAPI/Swagger** — API documentation schemas for dynamic forms\n- **Custom schemas** — Design your own format tailored to your domain\n\nSee the [Custom Schema guide](/docs/custom-schema) to learn how to implement support for any schema.\n\n## Built-in Schema\n\n`@json-render/react` uses a flat element tree schema with a root key and elements map:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Dashboard\" },\n      \"children\": [\"text-1\", \"button-1\"]\n    },\n    \"text-1\": {\n      \"type\": \"Text\",\n      \"props\": { \"content\": { \"$state\": \"/user/name\" } },\n      \"children\": []\n    },\n    \"button-1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Click me\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Schema Components\n\n### Element Structure\n\nIn the built-in schema, each element in the elements map has this structure:\n\n```typescript\ninterface Element {\n  type: string;                // Component type from catalog\n  props: Record<string, any>;  // Component properties\n  children: string[];          // Array of child element keys\n  visible?: VisibilityCondition;  // Conditional display\n}\n```\n\n### Data Binding Syntax\n\nReference dynamic data using `$state` expressions in props. The value is a JSON Pointer path into the state model:\n\n```json\n{\n  \"type\": \"Text\",\n  \"props\": {\n    \"content\": { \"$state\": \"/user/name\" },\n    \"count\": { \"$state\": \"/items/count\" }\n  },\n  \"children\": []\n}\n```\n\njson-render also supports `$item` and `$index` expressions for lists, two-way binding via `$bindState` / `$bindItem`, and conditional props. See [Data Binding](/docs/data-binding) for the full reference.\n\n### Action Format\n\nActions are defined in the catalog and referenced from components. The renderer handles action execution:\n\n```typescript\n// In your catalog\nactions: {\n  navigate: {\n    params: z.object({ url: z.string() }),\n    description: 'Navigate to a URL',\n  },\n  apiCall: {\n    params: z.object({\n      endpoint: z.string(),\n      method: z.enum(['GET', 'POST', 'PUT', 'DELETE']),\n    }),\n    description: 'Make an API request',\n  },\n}\n```\n\n## Custom Schemas\n\n`@json-render/core` is schema-agnostic. You can define any JSON structure:\n\n```typescript\nimport { z } from 'zod';\n\n// Define your own element schema\nconst MyElementSchema = z.object({\n  component: z.string(),\n  settings: z.record(z.unknown()),\n  nested: z.array(z.lazy(() => MyElementSchema)).optional(),\n});\n\n// Define your own data binding format\nconst BoundValue = z.object({\n  literal: z.string().optional(),\n  source: z.string().optional(),  // e.g., \"/users/0/name\"\n});\n\n// Define your own action format\nconst ActionSchema = z.object({\n  name: z.string(),\n  context: z.record(z.unknown()).optional(),\n});\n```\n\n## Schema vs Catalog\n\nThe schema and catalog work together but serve different purposes:\n\n- **Schema** — Defines the JSON structure (how elements are organized)\n- **Catalog** — Defines available components and their props (what can be used)\n\nThe schema is the grammar; the catalog is the vocabulary.\n\n## Next\n\nLearn about [specs](/docs/specs) — the actual JSON documents that describe your UI.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/skills/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"docs/skills\");\n\n# Skills\n\njson-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.\n\n## Available Skills\n\n- **core** — Core schemas, catalogs, and AI prompt generation.\n- **react** — React renderer that turns JSON specs into React component trees.\n- **react-pdf** — PDF renderer using `@react-pdf/renderer`.\n- **react-email** — Email renderer that produces HTML or plain-text emails.\n- **react-native** — React Native renderer for native mobile UIs.\n- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).\n- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.\n- **remotion** — Remotion renderer for video generation from JSON timeline specs.\n- **vue** — Vue 3 renderer for Vue component trees.\n- **svelte** — Svelte 5 renderer for Svelte component trees.\n- **solid** — SolidJS renderer for fine-grained reactive component trees.\n- **codegen** — Code generation utilities for building custom exporters.\n- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.\n- **redux** — Redux adapter for json-render's `StateStore` interface.\n- **zustand** — Zustand adapter for json-render's `StateStore` interface.\n- **jotai** — Jotai adapter for json-render's `StateStore` interface.\n- **xstate** — XState Store adapter for json-render's `StateStore` interface.\n\n## Installation\n\n```bash\nnpx skills add vercel-labs/json-render --skill core\nnpx skills add vercel-labs/json-render --skill react\nnpx skills add vercel-labs/json-render --skill react-pdf\nnpx skills add vercel-labs/json-render --skill react-email\nnpx skills add vercel-labs/json-render --skill react-native\nnpx skills add vercel-labs/json-render --skill shadcn\nnpx skills add vercel-labs/json-render --skill image\nnpx skills add vercel-labs/json-render --skill remotion\nnpx skills add vercel-labs/json-render --skill vue\nnpx skills add vercel-labs/json-render --skill svelte\nnpx skills add vercel-labs/json-render --skill solid\nnpx skills add vercel-labs/json-render --skill codegen\nnpx skills add vercel-labs/json-render --skill mcp\nnpx skills add vercel-labs/json-render --skill redux\nnpx skills add vercel-labs/json-render --skill zustand\nnpx skills add vercel-labs/json-render --skill jotai\nnpx skills add vercel-labs/json-render --skill xstate\n```\n\nAfter installing, your AI agent will automatically activate the right skill when it encounters a matching request.\n\n## core\n\nThe foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.\n\n## react\n\nTeaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.\n\n## react-pdf\n\nTeaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.\n\n## react-email\n\nTeaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.\n\n## react-native\n\nTeaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.\n\n## shadcn\n\nTeaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.\n\n## image\n\nTeaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.\n\n## remotion\n\nTeaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.\n\n## vue\n\nTeaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.\n\n## svelte\n\nTeaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.\n\n## solid\n\nTeaches agents how to render JSON specs as SolidJS component trees. Covers Solid-specific reactive patterns, provider wiring, bindings, actions, and streaming.\n\n## codegen\n\nTeaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.\n\n## mcp\n\nTeaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.\n\n## redux\n\nTeaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.\n\n## zustand\n\nTeaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.\n\n## jotai\n\nTeaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.\n\n## xstate\n\nTeaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.\n\n## Source\n\nAll skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/specs/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/specs\")\n\n# Specs\n\nA spec is a JSON document that describes your UI.\n\n## What is a Spec?\n\nA spec (specification) is the actual JSON that describes a UI. It uses components from a [catalog](/docs/catalog) and can optionally follow a [schema](/docs/schemas). Specs can be:\n\n- Generated by AI in real-time\n- Stored in a database\n- Streamed progressively from a server\n- Hand-authored as JSON files\n\njson-render is schema-agnostic — your specs can follow any JSON structure you choose.\n\n## Example Specs\n\n### Simple Spec\n\nA basic spec using the `@json-render/react` schema. Note the flat structure with a `root` key and `elements` map:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Welcome\" },\n      \"children\": [\"text-1\"]\n    },\n    \"text-1\": {\n      \"type\": \"Text\",\n      \"props\": { \"content\": { \"$state\": \"/user/greeting\" } },\n      \"children\": []\n    }\n  }\n}\n```\n\n### Complex Spec\n\nA more complex spec with multiple nested elements:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"User Profile\", \"padding\": \"md\" },\n      \"children\": [\"row-1\", \"button-1\"]\n    },\n    \"row-1\": {\n      \"type\": \"Row\",\n      \"props\": { \"gap\": \"md\" },\n      \"children\": [\"avatar-1\", \"stack-1\"]\n    },\n    \"avatar-1\": {\n      \"type\": \"Avatar\",\n      \"props\": { \"src\": { \"$state\": \"/user/avatar\" }, \"alt\": { \"$state\": \"/user/name\" } },\n      \"children\": []\n    },\n    \"stack-1\": {\n      \"type\": \"Stack\",\n      \"props\": { \"gap\": \"sm\" },\n      \"children\": [\"name-text\", \"email-text\"]\n    },\n    \"name-text\": {\n      \"type\": \"Text\",\n      \"props\": { \"content\": { \"$state\": \"/user/name\" }, \"variant\": \"heading\" },\n      \"children\": []\n    },\n    \"email-text\": {\n      \"type\": \"Text\",\n      \"props\": { \"content\": { \"$state\": \"/user/email\" }, \"variant\": \"caption\" },\n      \"children\": []\n    },\n    \"button-1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Edit Profile\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n### Block-Level Spec\n\nA high-level spec using semantic blocks for page layouts:\n\n```json\n{\n  \"root\": \"page\",\n  \"elements\": {\n    \"page\": {\n      \"type\": \"Page\",\n      \"props\": {},\n      \"children\": [\"header\", \"hero\", \"features\", \"footer\"]\n    },\n    \"header\": {\n      \"type\": \"Header\",\n      \"props\": { \"logo\": \"/logo.svg\", \"navItems\": [\"Products\", \"Pricing\", \"Docs\"] },\n      \"children\": []\n    },\n    \"hero\": {\n      \"type\": \"Hero\",\n      \"props\": {\n        \"title\": \"Build UIs with JSON\",\n        \"subtitle\": \"Let AI generate your interfaces\",\n        \"ctaLabel\": \"Get Started\",\n        \"ctaHref\": \"/docs\"\n      },\n      \"children\": []\n    },\n    \"features\": {\n      \"type\": \"Features\",\n      \"props\": { \"columns\": 3 },\n      \"children\": [\"feature-1\", \"feature-2\", \"feature-3\"]\n    },\n    \"feature-1\": {\n      \"type\": \"Feature\",\n      \"props\": { \"icon\": \"zap\", \"title\": \"Fast\", \"description\": \"Render UIs in milliseconds\" },\n      \"children\": []\n    },\n    \"feature-2\": {\n      \"type\": \"Feature\",\n      \"props\": { \"icon\": \"shield\", \"title\": \"Secure\", \"description\": \"Validate all specs against your catalog\" },\n      \"children\": []\n    },\n    \"feature-3\": {\n      \"type\": \"Feature\",\n      \"props\": { \"icon\": \"sparkles\", \"title\": \"AI-Ready\", \"description\": \"Generate prompts from your catalog\" },\n      \"children\": []\n    },\n    \"footer\": {\n      \"type\": \"Footer\",\n      \"props\": { \"copyright\": \"2025 Acme Inc\", \"links\": [\"Privacy\", \"Terms\", \"Contact\"] },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Spec Anatomy\n\nSpecs are schema-agnostic — the JSON structure is entirely up to you. The examples below use the `root` + `elements` flat tree format from the `@json-render/react` schema, which is optimized for AI generation and streaming.\n\n### Root and Elements\n\nIn the React schema, a spec has a `root` key pointing to the entry element, and an `elements` map containing all elements:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"My Card\" },\n      \"children\": [\"text-1\"]\n    },\n    \"text-1\": { ... }\n  }\n}\n```\n\n### Element Structure\n\nEach element in the map has a consistent shape:\n\n```json\n{\n  \"type\": \"ComponentName\",\n  \"props\": { \"label\": \"Hello\" },\n  \"children\": [\"child-1\", \"child-2\"]\n}\n```\n\n- `type` — Component type from your catalog\n- `props` — Component properties\n- `children` — Array of child element keys\n\n### Dynamic Data\n\nProps can reference data from the state model using `$state` expressions. The value is a JSON Pointer (RFC 6901) path into the state:\n\n```json\n{\n  \"type\": \"Metric\",\n  \"props\": {\n    \"label\": \"Total Revenue\",\n    \"value\": { \"$state\": \"/metrics/revenue\" },\n    \"change\": { \"$state\": \"/metrics/revenueChange\" }\n  },\n  \"children\": []\n}\n```\n\nSee [Data Binding](/docs/data-binding) for the full reference including `$item`, `$index`, repeat, and two-way binding.\n\n### Conditional Visibility\n\nControl when elements appear using the `visible` property:\n\n```json\n{\n  \"type\": \"Alert\",\n  \"props\": {\n    \"message\": \"You have unsaved changes\"\n  },\n  \"children\": [],\n  \"visible\": {\n    \"$state\": \"/form/isDirty\",\n    \"eq\": true\n  }\n}\n```\n\n## Working with Specs\n\n### Validating a Spec\n\nUse `validateSpec` from `@json-render/core` to check a spec for structural issues:\n\n```typescript\nimport { validateSpec } from '@json-render/core';\n\nconst result = validateSpec(spec);\n\nif (!result.valid) {\n  console.error('Invalid spec:', result.issues);\n}\n```\n\n### Rendering a Spec (React)\n\nWith `@json-render/react`, wrap the `Renderer` in providers to supply state and visibility:\n\n```tsx\nimport { Renderer, StateProvider, VisibilityProvider } from '@json-render/react';\nimport { registry } from './registry';\n\nfunction MyApp({ spec, initialState }) {\n  return (\n    <StateProvider initialState={initialState}>\n      <VisibilityProvider>\n        <Renderer spec={spec} registry={registry} />\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\nSee the [@json-render/react API reference](/docs/api/react) for full provider and hook documentation.\n\n### Streaming a Spec (React)\n\nWith `@json-render/react`, use the `useUIStream` hook to stream specs incrementally:\n\n```tsx\nimport { useUIStream } from '@json-render/react';\n\nfunction GenerativeUI() {\n  const { spec, isStreaming } = useUIStream({\n    api: '/api/generate',\n  });\n\n  return (\n    <Renderer\n      spec={spec}\n      registry={registry}\n      loading={isStreaming}\n    />\n  );\n}\n```\n\nSee [Streaming](/docs/streaming) for the full SpecStream format and server-side setup.\n\n## Spec Sources\n\nSpecs can come from various sources:\n\n- **AI Generation** — LLMs generate specs based on prompts and catalog\n- **Database** — Store specs as JSON and load dynamically\n- **API Response** — Server returns specs based on user/context\n- **Static Files** — Pre-built specs for known UI patterns\n\n## Next\n\nLearn about [catalogs](/docs/catalog) — the vocabulary of components available in your specs.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/streaming/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/streaming\")\n\n# Streaming\n\nProgressively render UI as AI generates it.\n\n## SpecStream Format\n\njson-render uses **SpecStream**, a JSONL-based streaming format where each line is a JSON patch operation that progressively builds your spec:\n\n```json\n{\"op\":\"add\",\"path\":\"/root\",\"value\":\"root\"}\n{\"op\":\"add\",\"path\":\"/elements/root\",\"value\":{\"type\":\"Card\",\"props\":{\"title\":\"Dashboard\"},\"children\":[\"metric-1\",\"metric-2\"]}}\n{\"op\":\"add\",\"path\":\"/elements/metric-1\",\"value\":{\"type\":\"Metric\",\"props\":{\"label\":\"Revenue\"}}}\n{\"op\":\"add\",\"path\":\"/elements/metric-2\",\"value\":{\"type\":\"Metric\",\"props\":{\"label\":\"Users\"}}}\n```\n\n## Patch Operations (RFC 6902)\n\nSpecStream uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations:\n\n- `add` — Add a value at a path (creates or replaces for objects, inserts for arrays)\n- `remove` — Remove the value at a path\n- `replace` — Replace an existing value at a path\n- `move` — Move a value from one path to another (requires `from`)\n- `copy` — Copy a value from one path to another (requires `from`)\n- `test` — Assert that a value at a path equals the given value\n\n## Path Format\n\nPaths follow JSON Pointer (RFC 6901) into the spec object:\n\n```bash\n/root                       -> Root element key (string)\n/elements/card-1            -> Element with key \"card-1\"\n/elements/card-1/props      -> Props of card-1\n/elements/card-1/children   -> Children of card-1\n```\n\n## Server-Side Setup\n\nEnsure your API route streams properly:\n\n```typescript\nimport { streamText } from 'ai';\nimport { catalog } from '@/lib/catalog';\n\nexport async function POST(req: Request) {\n  const { prompt } = await req.json();\n  \n  const result = streamText({\n    model: 'anthropic/claude-haiku-4.5',\n    system: catalog.prompt(),\n    prompt,\n  });\n\n  // Return as a streaming response\n  return result.toTextStreamResponse();\n}\n```\n\n## Low-Level SpecStream API\n\nFor custom or framework-agnostic streaming implementations, use the SpecStream compiler from `@json-render/core` directly:\n\n```typescript\nimport { createSpecStreamCompiler } from '@json-render/core';\n\n// Create a compiler for your spec type\nconst compiler = createSpecStreamCompiler<MySpec>();\nconst decoder = new TextDecoder();\n\n// Process streaming chunks from AI\nasync function processStream(reader: ReadableStreamDefaultReader<Uint8Array>) {\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    // Decode the Uint8Array chunk to a string\n    const chunk = decoder.decode(value, { stream: true });\n    const { result, newPatches } = compiler.push(chunk);\n    \n    if (newPatches.length > 0) {\n      // Update UI with partial result\n      setSpec(result);\n    }\n  }\n  \n  // Get final compiled result\n  return compiler.getResult();\n}\n```\n\n### One-Shot Compilation\n\nFor non-streaming scenarios, compile entire SpecStream at once:\n\n```typescript\nimport { compileSpecStream } from '@json-render/core';\n\nconst jsonl = `{\"op\":\"add\",\"path\":\"/root\",\"value\":\"card-1\"}\n{\"op\":\"add\",\"path\":\"/elements/card-1\",\"value\":{\"type\":\"Card\",\"props\":{\"title\":\"Hello\"},\"children\":[]}}`;\n\nconst spec = compileSpecStream<Spec>(jsonl);\n// { root: \"card-1\", elements: { \"card-1\": { type: \"Card\", props: { title: \"Hello\" }, children: [] } } }\n```\n\n## Usage with React\n\n`@json-render/react` provides the `useUIStream` hook, which wraps the low-level compiler in a React-friendly API with state management, error handling, and abort support.\n\n### useUIStream Hook\n\n```tsx\nimport { useUIStream } from '@json-render/react';\n\nfunction App() {\n  const {\n    spec,          // Current UI spec state\n    isStreaming,   // True while streaming\n    error,         // Any error that occurred\n    send,          // Function to start generation\n    clear,         // Function to reset spec and error\n  } = useUIStream({\n    api: '/api/generate',\n    onComplete: (spec) => {},  // Optional: called when streaming completes\n    onError: (error) => {},    // Optional: called when an error occurs\n  });\n}\n```\n\n### Progressive Rendering\n\nThe Renderer automatically updates as the spec changes:\n\n```tsx\nfunction App() {\n  const { spec, isStreaming } = useUIStream({ api: '/api/generate' });\n\n  return (\n    <div>\n      {isStreaming && <LoadingIndicator />}\n      <Renderer spec={spec} registry={registry} loading={isStreaming} />\n    </div>\n  );\n}\n```\n\n### Aborting Streams\n\nCalling `send` again automatically aborts the previous request. Use `clear` to reset the spec and error state:\n\n```tsx\nfunction App() {\n  const { isStreaming, send, clear } = useUIStream({\n    api: '/api/generate',\n  });\n\n  return (\n    <div>\n      <button onClick={() => send('Create dashboard')}>\n        Generate\n      </button>\n      <button onClick={clear}>Reset</button>\n    </div>\n  );\n}\n```\n\nSee the [@json-render/react API reference](/docs/api/react) for full `useUIStream` documentation.\n"
  },
  {
    "path": "apps/web/app/(main)/docs/validation/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/validation\")\n\n# Validation\n\nValidate form inputs with built-in and custom functions.\n\n## Built-in Validators\n\njson-render includes common validation functions:\n\n- `required` — Value must be non-empty\n- `email` — Valid email format\n- `minLength` — Minimum string length (args: `{ \"min\": N }`)\n- `maxLength` — Maximum string length (args: `{ \"max\": N }`)\n- `pattern` — Match a regex pattern (args: `{ \"pattern\": \"regex\" }`)\n- `min` — Minimum numeric value (args: `{ \"min\": N }`)\n- `max` — Maximum numeric value (args: `{ \"max\": N }`)\n- `numeric` — Value must be a number\n- `url` — Valid URL format\n- `matches` — Must equal another field (args: `{ \"other\": { \"$state\": \"/path\" } }`)\n- `equalTo` — Alias for matches (args: `{ \"other\": { \"$state\": \"/path\" } }`)\n- `lessThan` — Value must be less than another field (args: `{ \"other\": { \"$state\": \"/path\" } }`)\n- `greaterThan` — Value must be greater than another field (args: `{ \"other\": { \"$state\": \"/path\" } }`)\n- `requiredIf` — Required only when another field is truthy (args: `{ \"field\": { \"$state\": \"/path\" } }`)\n\n## Using Validation in JSON\n\nUse `{ \"$bindState\": \"/path\" }` on the value prop for two-way binding. Validation checks run against the value at the bound path (available as `bindings?.value` in components):\n\n```json\n{\n  \"type\": \"TextField\",\n  \"props\": {\n    \"label\": \"Email\",\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"checks\": [\n      { \"type\": \"required\", \"message\": \"Email is required\" },\n      { \"type\": \"email\", \"message\": \"Invalid email format\" }\n    ],\n    \"validateOn\": \"blur\"\n  }\n}\n```\n\n## Validation with Parameters\n\n```json\n{\n  \"type\": \"TextField\",\n  \"props\": {\n    \"label\": \"Password\",\n    \"value\": { \"$bindState\": \"/form/password\" },\n    \"checks\": [\n      { \"type\": \"required\", \"message\": \"Password is required\" },\n      { \n        \"type\": \"minLength\", \n        \"args\": { \"min\": 8 },\n        \"message\": \"Password must be at least 8 characters\"\n      },\n      {\n        \"type\": \"pattern\",\n        \"args\": { \"pattern\": \"[A-Z]\" },\n        \"message\": \"Must contain at least one uppercase letter\"\n      }\n    ]\n  }\n}\n```\n\n## Custom Validation Functions\n\nDefine custom validators in your catalog's `functions` field. The catalog itself is framework-agnostic — only the `schema` import varies by platform:\n\n```typescript\nimport { defineCatalog } from '@json-render/core';\nimport { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'\nimport { z } from 'zod';\n\nconst catalog = defineCatalog(schema, {\n  components: { /* ... */ },\n  functions: {\n    isValidPhone: {\n      description: 'Validates phone number format',\n    },\n    isUniqueEmail: {\n      description: 'Checks if email is not already registered',\n    },\n  },\n});\n```\n\n## Usage with React\n\nIn `@json-render/react`, use `ValidationProvider` to supply implementations for your custom validators:\n\n```tsx\nimport { ValidationProvider } from '@json-render/react';\n\nfunction App() {\n  const customValidators = {\n    isValidPhone: (value) => {\n      const phoneRegex = /^\\+?[1-9]\\d{1,14}$/;\n      return phoneRegex.test(value);\n    },\n    isUniqueEmail: async (value) => {\n      const response = await fetch(`/api/check-email?email=${value}`);\n      const { available } = await response.json();\n      return available;\n    },\n  };\n\n  return (\n    <ValidationProvider customFunctions={customValidators}>\n      {/* Your UI */}\n    </ValidationProvider>\n  );\n}\n```\n\n### Using in Components\n\nThe `useFieldValidation` and `useBoundProp` hooks wire validation into your registry components. Validation uses the path from `bindings?.value` (the bound state path):\n\n```tsx\nimport { useFieldValidation, useBoundProp } from '@json-render/react';\n\nfunction TextField({ props, bindings }) {\n  const [value, setValue] = useBoundProp(props.value, bindings?.value);\n  const { errors, isValid, validate, touch, clear } = useFieldValidation(\n    bindings?.value ?? null,\n    { checks: props.checks, validateOn: props.validateOn }\n  );\n\n  return (\n    <div>\n      <label>{props.label}</label>\n      <input\n        value={value || ''}\n        onChange={(e) => setValue(e.target.value)}\n        onBlur={() => validate()}\n      />\n      {errors.map((error, i) => (\n        <p key={i} className=\"text-red-500 text-sm\">{error}</p>\n      ))}\n    </div>\n  );\n}\n```\n\nSee the [@json-render/react API reference](/docs/api/react) for full `ValidationProvider` and `useFieldValidation` documentation.\n\n## Cross-Field Validation\n\nValidation args support `{ \"$state\": \"/path\" }` references to compare against other fields. This enables cross-field rules like \"confirm password must match password\":\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"label\": \"Confirm Password\",\n    \"value\": { \"$bindState\": \"/form/confirmPassword\" },\n    \"checks\": [\n      { \"type\": \"required\", \"message\": \"Please confirm your password\" },\n      {\n        \"type\": \"matches\",\n        \"args\": { \"other\": { \"$state\": \"/form/password\" } },\n        \"message\": \"Passwords must match\"\n      }\n    ]\n  }\n}\n```\n\nOther cross-field examples:\n\n```json\n{\n  \"checks\": [\n    {\n      \"type\": \"greaterThan\",\n      \"args\": { \"other\": { \"$state\": \"/form/startDate\" } },\n      \"message\": \"End date must be after start date\"\n    }\n  ]\n}\n```\n\n```json\n{\n  \"checks\": [\n    {\n      \"type\": \"requiredIf\",\n      \"args\": { \"field\": { \"$state\": \"/form/enableNotifications\" } },\n      \"message\": \"Email is required when notifications are enabled\"\n    }\n  ]\n}\n```\n\n## Conditional Validation\n\nUse the `enabled` field in the validation config to only run checks when a condition is met:\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"label\": \"Company Name\",\n    \"value\": { \"$bindState\": \"/form/company\" },\n    \"checks\": [\n      { \"type\": \"required\", \"message\": \"Company name is required\" }\n    ]\n  }\n}\n```\n\nIn the component implementation, you can pass `enabled` to `useFieldValidation`:\n\n```typescript\nuseFieldValidation(bindings?.value ?? \"\", {\n  checks: props.checks ?? [],\n  enabled: { \"$state\": \"/form/accountType\", eq: \"business\" },\n});\n```\n\nThis only validates the company name when the account type is \"business\".\n\n## Validation Timing\n\nControl when validation runs with `validateOn`:\n\n- `change` — Validate on every input change\n- `blur` — Validate when field loses focus (default for Input, Textarea)\n- `submit` — Validate only on form submission\n\n## Form-Level Validation\n\nUse the built-in `validateForm` action to validate all registered fields at once. This is useful for a \"Submit\" button that should validate the entire form before proceeding:\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": {\n    \"press\": [\n      { \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } },\n      { \"action\": \"submitForm\" }\n    ]\n  },\n  \"children\": []\n}\n```\n\nThe `validateForm` action runs `validateAll()` and writes `{ valid: boolean }` to the specified state path (defaults to `/formValidation`). Your submit handler can then check `{ \"$state\": \"/formResult/valid\" }` to decide whether to proceed.\n\n> **Note:** Actions in a list execute sequentially, but `submitForm` does not automatically gate on validation. Guard submission with a `$cond` visibility condition on the button or check `{ \"$state\": \"/formResult/valid\" }` inside your action handler to skip submission when the form is invalid.\n\n## Next\n\n- [Computed Values](/docs/computed-values) — derive dynamic prop values\n- [Watchers](/docs/watchers) — react to state changes\n- [Generation Modes](/docs/generation-modes) — how AI generates specs\n"
  },
  {
    "path": "apps/web/app/(main)/docs/visibility/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/visibility\")\n\n# Visibility\n\nConditionally show or hide components based on state values and logic.\n\n## State-Based Visibility\n\nShow/hide based on state values. Use `$state` with a JSON Pointer path:\n\n```json\n{\n  \"type\": \"Alert\",\n  \"props\": { \"message\": \"Form has errors\" },\n  \"visible\": { \"$state\": \"/form/hasErrors\" }\n}\n```\n\nVisible when `/form/hasErrors` is truthy.\n\n### Negation\n\nUse `not: true` to invert a condition:\n\n```json\n{\n  \"type\": \"WelcomeBanner\",\n  \"visible\": { \"$state\": \"/user/hasSeenWelcome\", \"not\": true }\n}\n```\n\nVisible when `/user/hasSeenWelcome` is falsy.\n\n## Auth-Based Visibility\n\nShow/hide based on authentication state. Expose your auth state in the state model (e.g. at `/auth/isSignedIn`):\n\n```json\n{\n  \"type\": \"AdminPanel\",\n  \"visible\": { \"$state\": \"/auth/isSignedIn\" }\n}\n```\n\nFor signed-out only:\n\n```json\n{\n  \"type\": \"LoginPrompt\",\n  \"visible\": { \"$state\": \"/auth/isSignedIn\", \"not\": true }\n}\n```\n\n## Comparison Operators\n\nCompare a state value to a literal or another state path. Use **one operator per condition** -- if multiple are provided, only the first one is evaluated (precedence: `eq` > `neq` > `gt` > `gte` > `lt` > `lte`). Add `\"not\": true` to invert the result of any condition.\n\n```json\n// Equal\n{\n  \"visible\": { \"$state\": \"/user/role\", \"eq\": \"admin\" }\n}\n\n// Not equal\n{\n  \"visible\": { \"$state\": \"/tab\", \"neq\": \"home\" }\n}\n\n// Greater than\n{\n  \"visible\": { \"$state\": \"/cart/total\", \"gt\": 100 }\n}\n\n// Greater than or equal\n{\n  \"visible\": { \"$state\": \"/cart/itemCount\", \"gte\": 1 }\n}\n\n// Less than\n{\n  \"visible\": { \"$state\": \"/cart/total\", \"lt\": 1000 }\n}\n\n// Less than or equal\n{\n  \"visible\": { \"$state\": \"/cart/itemCount\", \"lte\": 10 }\n}\n```\n\nComparison values can be literals or state references:\n\n```json\n{\n  \"visible\": { \"$state\": \"/user/balance\", \"gte\": { \"$state\": \"/order/minimum\" } }\n}\n```\n\n## Combining Conditions (AND)\n\nPlace multiple conditions in an array for implicit AND:\n\n```json\n{\n  \"type\": \"SubmitButton\",\n  \"visible\": [\n    { \"$state\": \"/form/isValid\" },\n    { \"$state\": \"/form/hasChanges\" }\n  ]\n}\n```\n\nAll conditions must be true for the element to be visible.\n\n## OR Conditions\n\nUse `$or` when at least one condition should be true:\n\n```json\n{\n  \"type\": \"SpecialOffer\",\n  \"visible\": { \"$or\": [\n    { \"$state\": \"/user/isVIP\" },\n    { \"$state\": \"/cart/total\", \"gt\": 200 }\n  ]}\n}\n```\n\nVisible when the user is VIP **or** the cart total exceeds 200. `$or` can contain any visibility conditions, including nested arrays (AND) and comparisons.\n\n## Explicit AND\n\nUse `$and` when you need to nest AND logic inside `$or`:\n\n```json\n{\n  \"type\": \"PromoCard\",\n  \"visible\": { \"$or\": [\n    { \"$and\": [\n      { \"$state\": \"/user/isVIP\" },\n      { \"$state\": \"/cart/total\", \"gt\": 50 }\n    ]},\n    { \"$state\": \"/promo/active\" }\n  ]}\n}\n```\n\nFor top-level AND, the implicit array form is simpler: `[condition, condition]`. Use `$and` only when nesting inside `$or`.\n\n## Always / Never\n\nUse boolean literals for constant visibility:\n\n```json\n{\n  \"type\": \"Footer\",\n  \"visible\": true\n}\n```\n\n```json\n{\n  \"type\": \"DeprecatedPanel\",\n  \"visible\": false\n}\n```\n\n## Repeat-Scoped Conditions\n\nInside a [repeat](/docs/data-binding#repeat), use `$item` and `$index` conditions to show/hide based on the current item:\n\n### `$item` — Condition on item field\n\n```json\n{\n  \"type\": \"Badge\",\n  \"props\": { \"label\": \"Overdue\" },\n  \"visible\": { \"$item\": \"isOverdue\" }\n}\n```\n\nWith comparison:\n\n```json\n{\n  \"type\": \"DiscountTag\",\n  \"visible\": { \"$item\": \"price\", \"gt\": 100 }\n}\n```\n\n### `$index` — Condition on array index\n\n```json\n{\n  \"type\": \"Divider\",\n  \"visible\": { \"$index\": true, \"gt\": 0 }\n}\n```\n\nThis shows the divider for every item except the first (index 0).\n\n`$item` and `$index` conditions support the same comparison operators as `$state` (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`).\n\n## Complex Example\n\n```json\n{\n  \"type\": \"RefundButton\",\n  \"props\": { \"label\": \"Process Refund\" },\n  \"visible\": [\n    { \"$state\": \"/auth/isSignedIn\" },\n    { \"$state\": \"/user/role\", \"eq\": \"support\" },\n    { \"$state\": \"/order/amount\", \"gt\": 0 },\n    { \"$state\": \"/order/isRefunded\", \"not\": true }\n  ]\n}\n```\n\n## Quick Reference\n\n<div className=\"my-6 overflow-x-auto\">\n  <table className=\"mdx-table w-full text-sm border-collapse\">\n    <thead>\n      <tr>\n        <th>Condition</th>\n        <th>Syntax</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>Truthiness</td>\n        <td><code>{'{ \"$state\": \"/path\" }'}</code></td>\n      </tr>\n      <tr>\n        <td>Falsy (not)</td>\n        <td><code>{'{ \"$state\": \"/path\", \"not\": true }'}</code></td>\n      </tr>\n      <tr>\n        <td>Equal</td>\n        <td><code>{'{ \"$state\": \"/path\", \"eq\": value }'}</code></td>\n      </tr>\n      <tr>\n        <td>Not equal</td>\n        <td><code>{'{ \"$state\": \"/path\", \"neq\": value }'}</code></td>\n      </tr>\n      <tr>\n        <td>Greater than</td>\n        <td><code>{'{ \"$state\": \"/path\", \"gt\": number }'}</code></td>\n      </tr>\n      <tr>\n        <td>Greater or equal</td>\n        <td><code>{'{ \"$state\": \"/path\", \"gte\": number }'}</code></td>\n      </tr>\n      <tr>\n        <td>Less than</td>\n        <td><code>{'{ \"$state\": \"/path\", \"lt\": number }'}</code></td>\n      </tr>\n      <tr>\n        <td>Less or equal</td>\n        <td><code>{'{ \"$state\": \"/path\", \"lte\": number }'}</code></td>\n      </tr>\n      <tr>\n        <td>Item field (repeat)</td>\n        <td><code>{'{ \"$item\": \"field\" }'}</code></td>\n      </tr>\n      <tr>\n        <td>Item comparison</td>\n        <td><code>{'{ \"$item\": \"field\", \"eq\": value }'}</code></td>\n      </tr>\n      <tr>\n        <td>Index (repeat)</td>\n        <td><code>{'{ \"$index\": true, \"gt\": 0 }'}</code></td>\n      </tr>\n      <tr>\n        <td>AND (implicit)</td>\n        <td><code>{\"[ condition, condition ]\"}</code></td>\n      </tr>\n      <tr>\n        <td>AND (explicit)</td>\n        <td><code>{'{ \"$and\": [ condition, condition ] }'}</code></td>\n      </tr>\n      <tr>\n        <td>OR</td>\n        <td><code>{'{ \"$or\": [ condition, condition ] }'}</code></td>\n      </tr>\n      <tr>\n        <td>Always</td>\n        <td><code>{\"true\"}</code></td>\n      </tr>\n      <tr>\n        <td>Never</td>\n        <td><code>{\"false\"}</code></td>\n      </tr>\n    </tbody>\n  </table>\n</div>\n\nComparison values can be literals or state references for state-to-state comparisons:\n\n```json\n{ \"$state\": \"/a\", \"eq\": { \"$state\": \"/b\" } }\n```\n\n## Usage with React\n\nIn `@json-render/react`, wrap your app with `VisibilityProvider` to enable conditional rendering. The `Renderer` handles visibility automatically — elements with unmet conditions are not rendered.\n\n```tsx\nimport { VisibilityProvider, StateProvider } from '@json-render/react';\n\nfunction App() {\n  return (\n    <StateProvider initialState={data}>\n      <VisibilityProvider>\n        {/* Components can now use visibility conditions */}\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\nFor advanced use cases, the `useIsVisible` hook lets you evaluate visibility conditions programmatically:\n\n```tsx\nimport { useIsVisible } from '@json-render/react';\n\nfunction ConditionalContent({ condition, children }) {\n  const isVisible = useIsVisible(condition);\n\n  if (!isVisible) return null;\n  return <div>{children}</div>;\n}\n```\n\nSee the [@json-render/react API reference](/docs/api/react) for full details.\n\n## Next\n\nLearn about [form validation](/docs/validation).\n"
  },
  {
    "path": "apps/web/app/(main)/docs/watchers/page.mdx",
    "content": "import { pageMetadata } from \"@/lib/page-metadata\"\nexport const metadata = pageMetadata(\"docs/watchers\")\n\n# Watchers\n\nReact to state changes by triggering actions when watched paths update.\n\n## The `watch` Field\n\nElements can have an optional `watch` field that maps state paths to action bindings. When the value at a watched path changes, the bound actions fire automatically.\n\n`watch` is a **top-level field** on the element (sibling of `type`, `props`, `children`) — not inside `props`.\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"label\": \"Country\",\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\", \"UK\"]\n  },\n  \"watch\": {\n    \"/form/country\": {\n      \"action\": \"loadCities\",\n      \"params\": { \"country\": { \"$state\": \"/form/country\" } }\n    }\n  },\n  \"children\": []\n}\n```\n\nWhen the user selects a different country, the `loadCities` action fires with the new country value. The action handler can fetch city data and update state, causing a dependent city Select to re-render with new options.\n\n## Cascading Selects\n\nA common pattern is cascading dropdowns where selecting a value in one field loads options for another:\n\n```json\n{\n  \"root\": \"form\",\n  \"elements\": {\n    \"form\": {\n      \"type\": \"Stack\",\n      \"props\": { \"direction\": \"vertical\", \"gap\": \"md\" },\n      \"children\": [\"country-select\", \"city-select\"]\n    },\n    \"country-select\": {\n      \"type\": \"Select\",\n      \"props\": {\n        \"label\": \"Country\",\n        \"value\": { \"$bindState\": \"/form/country\" },\n        \"options\": [\"US\", \"Canada\", \"UK\"]\n      },\n      \"watch\": {\n        \"/form/country\": [\n          { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } },\n          { \"action\": \"setState\", \"params\": { \"statePath\": \"/form/city\", \"value\": \"\" } }\n        ]\n      },\n      \"children\": []\n    },\n    \"city-select\": {\n      \"type\": \"Select\",\n      \"props\": {\n        \"label\": \"City\",\n        \"value\": { \"$bindState\": \"/form/city\" },\n        \"options\": { \"$state\": \"/availableCities\" },\n        \"placeholder\": \"Select a city\"\n      },\n      \"children\": []\n    }\n  },\n  \"state\": {\n    \"form\": { \"country\": \"\", \"city\": \"\" },\n    \"availableCities\": []\n  }\n}\n```\n\nThe watcher on `country-select` fires two actions when the country changes:\n1. `loadCities` — fetches and writes city options to `/availableCities`\n2. `setState` — resets the city selection\n\nThe city Select reads its options from `{ \"$state\": \"/availableCities\" }`, so it automatically updates when the data is loaded.\n\n### Action Handler\n\n```typescript\nconst handlers = {\n  loadCities: async (params) => {\n    const cities = await fetchCities(params.country);\n    // setState is called by the runtime to write the result\n    return cities;\n  },\n};\n```\n\nOr with `defineRegistry`:\n\n```typescript\nconst { registry, handlers } = defineRegistry(catalog, {\n  components: { /* ... */ },\n  actions: {\n    loadCities: async (params, setState) => {\n      const response = await fetch(`/api/cities?country=${params.country}`);\n      const cities = await response.json();\n      setState('/availableCities', cities);\n    },\n  },\n});\n```\n\n## Multiple Watchers\n\nAn element can watch multiple state paths. Each path maps to one or more action bindings:\n\n```json\n{\n  \"watch\": {\n    \"/form/startDate\": { \"action\": \"validateDateRange\" },\n    \"/form/endDate\": { \"action\": \"validateDateRange\" },\n    \"/form/quantity\": [\n      { \"action\": \"recalculateTotal\" },\n      { \"action\": \"checkInventory\", \"params\": { \"qty\": { \"$state\": \"/form/quantity\" } } }\n    ]\n  }\n}\n```\n\n## Behavior\n\n- Watchers only fire on **value changes**, not on the initial render\n- Comparison is by reference (`===`), not deep equality\n- Action params support the same expressions as event bindings (`$state`, `$item`, `$index`)\n- Multiple action bindings on the same path execute sequentially\n\n## When to Use `watch` vs `on`\n\n<table>\n<thead>\n<tr>\n<th>Mechanism</th>\n<th>Trigger</th>\n<th>Use Case</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>on</code></td>\n<td>User interaction (press, change, blur)</td>\n<td>Button clicks, input changes, form submissions</td>\n</tr>\n<tr>\n<td><code>watch</code></td>\n<td>State value change (any source)</td>\n<td>Cascading data, derived state, cross-field sync</td>\n</tr>\n</tbody>\n</table>\n\nUse `on` when reacting to direct user actions. Use `watch` when a state change (from any source — user input, action handler, or external store update) should trigger side effects.\n\n## Next\n\n- [Data Binding](/docs/data-binding) — connect elements to state\n- [Computed Values](/docs/computed-values) — derive prop values\n- [Visibility](/docs/visibility) — conditionally show or hide elements\n"
  },
  {
    "path": "apps/web/app/(main)/examples/page.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { examples, allTags, getGitHubUrl, type Example } from \"@/lib/examples\";\nimport { cn } from \"@/lib/utils\";\n\nfunction ExampleCard({ example }: { example: Example }) {\n  return (\n    <div className=\"group flex flex-col rounded-xl border border-border bg-card text-card-foreground overflow-hidden transition-colors hover:border-foreground/25\">\n      <div className=\"flex flex-1 flex-col gap-3 p-5\">\n        <h3 className=\"font-semibold leading-none\">{example.title}</h3>\n\n        <p className=\"text-sm text-muted-foreground leading-relaxed\">\n          {example.description}\n        </p>\n\n        <div className=\"flex flex-wrap gap-1.5\">\n          {example.tags.map((tag) => (\n            <Badge key={tag} variant=\"secondary\" className=\"text-[11px]\">\n              {tag}\n            </Badge>\n          ))}\n        </div>\n\n        <div className=\"mt-auto flex items-center gap-3 pt-2\">\n          {example.demoUrl && (\n            <a\n              href={example.demoUrl}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary transition-colors\"\n            >\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"14\"\n                height=\"14\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M15 3h6v6\" />\n                <path d=\"M10 14 21 3\" />\n                <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n              </svg>\n              Live Demo\n            </a>\n          )}\n          <a\n            href={getGitHubUrl(example)}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <svg\n              viewBox=\"0 0 16 16\"\n              className=\"h-3.5 w-3.5\"\n              fill=\"currentColor\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n            </svg>\n            Source\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default function ExamplesPage() {\n  const [activeTag, setActiveTag] = useState<string | null>(null);\n\n  const filtered = activeTag\n    ? examples.filter((e) => e.tags.includes(activeTag))\n    : examples;\n\n  return (\n    <section className=\"mx-auto max-w-6xl px-6 py-16\">\n      <div className=\"mb-10\">\n        <h1 className=\"text-3xl font-bold tracking-tight sm:text-4xl\">\n          Examples\n        </h1>\n        <p className=\"mt-3 text-lg text-muted-foreground\">\n          Explore json-render across frameworks, renderers, and use cases.\n        </p>\n      </div>\n\n      <div className=\"mb-8 flex flex-wrap gap-2\">\n        <button\n          onClick={() => setActiveTag(null)}\n          className={cn(\n            \"rounded-full border px-3 py-1 text-xs font-medium transition-colors\",\n            activeTag === null\n              ? \"border-foreground bg-foreground text-background\"\n              : \"border-border text-muted-foreground hover:text-foreground hover:border-foreground/50\",\n          )}\n        >\n          All\n        </button>\n        {allTags.map((tag) => (\n          <button\n            key={tag}\n            onClick={() => setActiveTag(activeTag === tag ? null : tag)}\n            className={cn(\n              \"rounded-full border px-3 py-1 text-xs font-medium transition-colors\",\n              activeTag === tag\n                ? \"border-foreground bg-foreground text-background\"\n                : \"border-border text-muted-foreground hover:text-foreground hover:border-foreground/50\",\n            )}\n          >\n            {tag}\n          </button>\n        ))}\n      </div>\n\n      <div className=\"grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n        {filtered.map((example) => (\n          <ExampleCard key={example.slug} example={example} />\n        ))}\n      </div>\n\n      {filtered.length === 0 && (\n        <p className=\"py-12 text-center text-muted-foreground\">\n          No examples match the selected filter.\n        </p>\n      )}\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/(main)/layout.tsx",
    "content": "import { Header } from \"@/components/header\";\n\nexport default function MainLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"min-h-screen flex flex-col\">\n      <Header />\n      <main className=\"flex-1\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/(main)/page.tsx",
    "content": "import Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport { Demo } from \"@/components/demo\";\nimport { Code } from \"@/components/code\";\nimport { CopyButton } from \"@/components/copy-button\";\n\nexport default function Home() {\n  return (\n    <>\n      {/* Hero */}\n      <section className=\"max-w-5xl mx-auto px-6 pt-24 pb-16 text-center\">\n        <p className=\"text-xs sm:text-sm font-medium text-muted-foreground tracking-widest uppercase mb-4\">\n          The Generative UI Framework\n        </p>\n        <h1 className=\"text-4xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6\">\n          AI → json-render → UI\n        </h1>\n        <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed\">\n          Generate dynamic, personalized UIs from prompts without sacrificing\n          reliability. Predefined components and actions for safe, predictable\n          output.\n        </p>\n\n        <Demo />\n\n        <div className=\"flex items-center justify-center gap-2 border border-border rounded px-4 py-3 mt-12 mx-auto w-fit\">\n          <code className=\"text-sm bg-transparent\">\n            npm install @json-render/core @json-render/react\n          </code>\n          <CopyButton text=\"npm install @json-render/core @json-render/react\" />\n        </div>\n\n        <div className=\"flex gap-3 justify-center mt-6\">\n          <Button size=\"lg\" asChild>\n            <Link href=\"/docs\">Get Started</Link>\n          </Button>\n          <Button size=\"lg\" variant=\"outline\" asChild>\n            <a\n              href=\"https://github.com/vercel-labs/json-render\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"w-5 h-5\">\n                <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\" />\n              </svg>\n              GitHub\n            </a>\n          </Button>\n        </div>\n      </section>\n\n      {/* How it works */}\n      <section className=\"border-t border-border\">\n        <div className=\"max-w-5xl mx-auto px-6 py-24\">\n          <div className=\"grid md:grid-cols-3 gap-12\">\n            <div>\n              <div className=\"text-xs text-muted-foreground font-mono mb-3\">\n                01\n              </div>\n              <h3 className=\"text-lg font-semibold mb-2\">\n                Define Your Catalog\n              </h3>\n              <p className=\"text-sm text-muted-foreground leading-relaxed\">\n                Set the guardrails. Define which components, actions, and data\n                bindings AI can use.\n              </p>\n            </div>\n            <div>\n              <div className=\"text-xs text-muted-foreground font-mono mb-3\">\n                02\n              </div>\n              <h3 className=\"text-lg font-semibold mb-2\">AI Generates</h3>\n              <p className=\"text-sm text-muted-foreground leading-relaxed\">\n                Describe what you want. AI generates JSON constrained to your\n                catalog. Every interface is unique.\n              </p>\n            </div>\n            <div>\n              <div className=\"text-xs text-muted-foreground font-mono mb-3\">\n                03\n              </div>\n              <h3 className=\"text-lg font-semibold mb-2\">Render Instantly</h3>\n              <p className=\"text-sm text-muted-foreground leading-relaxed\">\n                Stream the response. Your components render progressively as\n                JSON arrives.\n              </p>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      {/* Code example */}\n      <section className=\"border-t border-border\">\n        <div className=\"max-w-5xl mx-auto px-6 py-24\">\n          <div className=\"grid lg:grid-cols-2 gap-12\">\n            <div className=\"min-w-0\">\n              <h2 className=\"text-2xl font-semibold mb-4\">\n                Define your catalog\n              </h2>\n              <p className=\"text-muted-foreground mb-6\">\n                Components, actions, and validation functions.\n              </p>\n              <Code lang=\"typescript\">{`import { defineSchema, defineCatalog } from '@json-render/core';\nimport { z } from 'zod';\n\nconst schema = defineSchema({ /* ... */ });\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      hasChildren: true,\n    },\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        statePath: z.string(),\n        format: z.enum(['currency', 'percent']),\n      }),\n    },\n  },\n  actions: {\n    export: { params: z.object({ format: z.string() }) },\n  },\n});`}</Code>\n            </div>\n            <div className=\"min-w-0\">\n              <h2 className=\"text-2xl font-semibold mb-4\">AI generates JSON</h2>\n              <p className=\"text-muted-foreground mb-6\">\n                Constrained output that your components render natively.\n              </p>\n              <Code lang=\"json\">{`{\n  \"root\": \"dashboard\",\n  \"elements\": {\n    \"dashboard\": {\n      \"type\": \"Card\",\n      \"props\": {\n        \"title\": \"Revenue Dashboard\"\n      },\n      \"children\": [\"revenue\"]\n    },\n    \"revenue\": {\n      \"type\": \"Metric\",\n      \"props\": {\n        \"label\": \"Total Revenue\",\n        \"statePath\": \"/metrics/revenue\",\n        \"format\": \"currency\"\n      }\n    }\n  }\n}`}</Code>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      {/* Code Export */}\n      <section className=\"border-t border-border\">\n        <div className=\"max-w-5xl mx-auto px-6 py-24\">\n          <div className=\"text-center mb-12\">\n            <h2 className=\"text-2xl font-semibold mb-4\">Export as Code</h2>\n            <p className=\"text-muted-foreground max-w-2xl mx-auto\">\n              Export generated UI as standalone React components. No runtime\n              dependencies required.\n            </p>\n          </div>\n          <div className=\"grid lg:grid-cols-2 gap-12\">\n            <div className=\"min-w-0\">\n              <h3 className=\"text-lg font-semibold mb-4\">Generated UI Tree</h3>\n              <p className=\"text-muted-foreground mb-6 text-sm\">\n                AI generates a JSON structure from the user&apos;s prompt.\n              </p>\n              <Code lang=\"json\">{`{\n  \"root\": \"card\",\n  \"elements\": {\n    \"card\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Revenue\" },\n      \"children\": [\"metric\", \"chart\"]\n    },\n    \"metric\": {\n      \"type\": \"Metric\",\n      \"props\": {\n        \"label\": \"Total Revenue\",\n        \"statePath\": \"analytics/revenue\",\n        \"format\": \"currency\"\n      }\n    },\n    \"chart\": {\n      \"type\": \"Chart\",\n      \"props\": {\n        \"statePath\": \"analytics/salesByRegion\"\n      }\n    }\n  }\n}`}</Code>\n            </div>\n            <div className=\"min-w-0\">\n              <h3 className=\"text-lg font-semibold mb-4\">\n                Exported React Code\n              </h3>\n              <p className=\"text-muted-foreground mb-6 text-sm\">\n                Export as a standalone Next.js project with all components.\n              </p>\n              <Code lang=\"tsx\">{`\"use client\";\n\nimport { Card, Metric, Chart } from \"@/components/ui\";\n\nconst data = {\n  analytics: {\n    revenue: 125000,\n    salesByRegion: [\n      { label: \"US\", value: 45000 },\n      { label: \"EU\", value: 35000 },\n    ],\n  },\n};\n\nexport default function Page() {\n  return (\n    <Card data={data} title=\"Revenue\">\n      <Metric\n        data={data}\n        label=\"Total Revenue\"\n        statePath=\"analytics/revenue\"\n        format=\"currency\"\n      />\n      <Chart data={data} statePath=\"analytics/salesByRegion\" />\n    </Card>\n  );\n}`}</Code>\n            </div>\n          </div>\n          <div className=\"mt-8 text-center\">\n            <p className=\"text-sm text-muted-foreground\">\n              The export includes{\" \"}\n              <code className=\"text-foreground\">package.json</code>, component\n              files, styles, and everything needed to run independently.\n            </p>\n          </div>\n        </div>\n      </section>\n\n      {/* Features */}\n      <section className=\"border-t border-border\">\n        <div className=\"max-w-5xl mx-auto px-6 py-24\">\n          <h2 className=\"text-2xl font-semibold mb-12 text-center\">Features</h2>\n          <div className=\"grid sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n            {[\n              {\n                title: \"Generative UI\",\n                desc: \"Generate dynamic, personalized interfaces from prompts with AI\",\n              },\n              {\n                title: \"Guardrails\",\n                desc: \"AI can only use components you define in the catalog\",\n              },\n              {\n                title: \"Streaming\",\n                desc: \"Progressive rendering as JSON streams from the model\",\n              },\n              {\n                title: \"React & React Native\",\n                desc: \"Render on web and mobile from the same catalog and spec format\",\n              },\n              {\n                title: \"Data Binding\",\n                desc: \"Connect props to state with $state, $item, $index, and two-way binding\",\n              },\n              {\n                title: \"Code Export\",\n                desc: \"Export as standalone React code with no runtime dependencies\",\n              },\n            ].map((feature) => (\n              <div key={feature.title}>\n                <h3 className=\"font-semibold mb-2\">{feature.title}</h3>\n                <p className=\"text-sm text-muted-foreground\">{feature.desc}</p>\n              </div>\n            ))}\n          </div>\n        </div>\n      </section>\n\n      {/* CTA */}\n      <section className=\"border-t border-border\">\n        <div className=\"max-w-4xl mx-auto px-6 py-24 text-center\">\n          <h2 className=\"text-2xl font-semibold mb-4\">Get started</h2>\n          <div className=\"flex items-center justify-center gap-2 border border-border rounded px-4 py-3 mb-8 mx-auto w-fit\">\n            <code className=\"text-sm bg-transparent\">\n              npm install @json-render/core @json-render/react\n            </code>\n            <CopyButton text=\"npm install @json-render/core @json-render/react\" />\n          </div>\n          <div>\n            <Button asChild>\n              <Link href=\"/docs\">Documentation</Link>\n            </Button>\n          </div>\n        </div>\n      </section>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/api/docs-chat/route.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { convertToModelMessages, stepCountIs, streamText } from \"ai\";\nimport type { ModelMessage, UIMessage } from \"ai\";\nimport { createBashTool } from \"bash-tool\";\nimport { headers } from \"next/headers\";\nimport { allDocsPages } from \"@/lib/docs-navigation\";\nimport { mdxToCleanMarkdown } from \"@/lib/mdx-to-markdown\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\n\nexport const maxDuration = 60;\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nconst SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render, a library for AI-generated UI with guardrails.\n\nGitHub repository: https://github.com/vercel-labs/json-render\nDocumentation: https://json-render.dev/docs\nnpm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml\nSkills: json-render ships AI agent skills that teach coding agents how to use each package. Install with \"npx skills add vercel-labs/json-render --skill <name>\". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.\n\nYou have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.\n\nWhen answering questions:\n- Use the bash tool to list files (ls /workspace/docs/) or search for content (grep -r \"keyword\" /workspace/docs/)\n- Use the readFile tool to read specific documentation pages (e.g. readFile with path \"/workspace/docs/index.md\")\n- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only\n- Always base your answers on the actual documentation content\n- Be concise and accurate\n- If the docs don't cover a topic, say so honestly\n- Do NOT include source references or file paths in your response\n- Do NOT use emojis in your responses`;\n\nasync function loadDocsFiles(): Promise<Record<string, string>> {\n  const files: Record<string, string> = {};\n\n  const results = await Promise.allSettled(\n    allDocsPages.map(async (page) => {\n      const slug =\n        page.href === \"/docs\" ? \"\" : page.href.replace(/^\\/docs\\/?/, \"\");\n      const filePath = slug\n        ? join(\n            process.cwd(),\n            \"app\",\n            \"(main)\",\n            \"docs\",\n            ...slug.split(\"/\"),\n            \"page.mdx\",\n          )\n        : join(process.cwd(), \"app\", \"(main)\", \"docs\", \"page.mdx\");\n\n      const raw = await readFile(filePath, \"utf-8\");\n      const md = mdxToCleanMarkdown(raw);\n      const fileName = slug ? `/docs/${slug}.md` : \"/docs/index.md\";\n      return { fileName, md };\n    }),\n  );\n\n  for (const result of results) {\n    if (result.status === \"fulfilled\") {\n      files[result.value.fileName] = result.value.md;\n    }\n  }\n\n  return files;\n}\n\nfunction addCacheControl(messages: ModelMessage[]): ModelMessage[] {\n  if (messages.length === 0) return messages;\n  return messages.map((message, index) => {\n    if (index === messages.length - 1) {\n      return {\n        ...message,\n        providerOptions: {\n          ...message.providerOptions,\n          anthropic: { cacheControl: { type: \"ephemeral\" } },\n        },\n      };\n    }\n    return message;\n  });\n}\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { messages }: { messages: UIMessage[] } = await req.json();\n\n  const docsFiles = await loadDocsFiles();\n  const {\n    tools: { bash, readFile },\n  } = await createBashTool({ files: docsFiles });\n\n  const result = streamText({\n    model: DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    messages: await convertToModelMessages(messages),\n    stopWhen: stepCountIs(5),\n    tools: {\n      bash,\n      readFile,\n    },\n    prepareStep: ({ messages: stepMessages }) => ({\n      messages: addCacheControl(stepMessages),\n    }),\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n"
  },
  {
    "path": "apps/web/app/api/docs-markdown/route.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { mdxToCleanMarkdown } from \"@/lib/mdx-to-markdown\";\n\nexport async function GET(req: NextRequest) {\n  const { searchParams } = new URL(req.url);\n  const docPath = searchParams.get(\"path\");\n\n  if (!docPath) {\n    return NextResponse.json(\n      { error: \"Missing ?path= parameter\" },\n      { status: 400 },\n    );\n  }\n\n  // Sanitize path: only allow docs paths, no traversal\n  const normalized = docPath\n    .replace(/^\\//, \"\")\n    .replace(/\\.\\./g, \"\")\n    .replace(/[^a-zA-Z0-9/-]/g, \"\");\n\n  if (!normalized.startsWith(\"docs\")) {\n    return NextResponse.json({ error: \"Invalid path\" }, { status: 400 });\n  }\n\n  // Map URL path to file path\n  // /docs -> /app/(main)/docs/page.mdx\n  // /docs/installation -> /app/(main)/docs/installation/page.mdx\n  const slug = normalized === \"docs\" ? \"\" : normalized.replace(/^docs\\/?/, \"\");\n  const filePath = slug\n    ? join(\n        process.cwd(),\n        \"app\",\n        \"(main)\",\n        \"docs\",\n        ...slug.split(\"/\"),\n        \"page.mdx\",\n      )\n    : join(process.cwd(), \"app\", \"(main)\", \"docs\", \"page.mdx\");\n\n  try {\n    const raw = await readFile(filePath, \"utf-8\");\n    const markdown = mdxToCleanMarkdown(raw);\n\n    return new NextResponse(markdown, {\n      headers: {\n        \"Content-Type\": \"text/markdown; charset=utf-8\",\n        \"Cache-Control\": \"public, max-age=3600\",\n      },\n    });\n  } catch {\n    return NextResponse.json({ error: \"Page not found\" }, { status: 404 });\n  }\n}\n"
  },
  {
    "path": "apps/web/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { headers } from \"next/headers\";\nimport type { Spec, EditMode } from \"@json-render/core\";\nimport {\n  buildUserPrompt,\n  buildEditUserPrompt,\n  isNonEmptySpec,\n} from \"@json-render/core\";\nimport { yamlPrompt } from \"@json-render/yaml\";\nimport { stringify as yamlStringify } from \"yaml\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { playgroundCatalog } from \"@/lib/render/catalog\";\n\nexport const maxDuration = 30;\n\nconst PLAYGROUND_RULES = [\n  \"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.\",\n  \"NEVER use page background colors (bg-gray-50) - the container has its own background.\",\n  \"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.\",\n  \"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.\",\n  \"Wrap each repeated item in a Card for visual separation and structure.\",\n  \"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.\",\n  'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { \"$bindState\": \"/path\" }).',\n];\n\nconst MAX_PROMPT_LENGTH = 500;\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nfunction getSystemPrompt(isYaml: boolean, editModes?: EditMode[]): string {\n  if (isYaml) {\n    return yamlPrompt(playgroundCatalog, {\n      mode: \"standalone\",\n      customRules: PLAYGROUND_RULES,\n      editModes: editModes ?? [\"merge\"],\n    });\n  }\n  return playgroundCatalog.prompt({\n    customRules: PLAYGROUND_RULES,\n    editModes,\n  });\n}\n\nfunction buildYamlUserPrompt(\n  prompt: string,\n  previousSpec?: Spec | null,\n  editModes?: EditMode[],\n): string {\n  if (isNonEmptySpec(previousSpec)) {\n    return buildEditUserPrompt({\n      prompt,\n      currentSpec: previousSpec,\n      config: { modes: editModes ?? [\"merge\"] },\n      format: \"yaml\",\n      maxPromptLength: MAX_PROMPT_LENGTH,\n      serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),\n    });\n  }\n\n  const userText = prompt.slice(0, MAX_PROMPT_LENGTH);\n  return [\n    userText,\n    \"\",\n    \"Output the full spec in a ```yaml-spec fence. Stream progressively — output elements one at a time.\",\n  ].join(\"\\n\");\n}\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt, context, format, editModes } = await req.json();\n  const isYaml = format === \"yaml\";\n\n  const systemPrompt = getSystemPrompt(isYaml, editModes);\n  const userPrompt = isYaml\n    ? buildYamlUserPrompt(prompt, context?.previousSpec, editModes)\n    : buildUserPrompt({\n        prompt,\n        currentSpec: context?.previousSpec,\n        maxPromptLength: MAX_PROMPT_LENGTH,\n        editModes,\n      });\n\n  const result = streamText({\n    model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,\n    system: [\n      {\n        role: \"system\",\n        content: systemPrompt,\n        providerOptions: {\n          anthropic: { cacheControl: { type: \"ephemeral\" } },\n        },\n      },\n    ],\n    prompt: userPrompt,\n    temperature: 0.7,\n  });\n\n  const encoder = new TextEncoder();\n  const textStream = result.textStream;\n\n  const stream = new ReadableStream({\n    async start(controller) {\n      for await (const chunk of textStream) {\n        controller.enqueue(encoder.encode(chunk));\n      }\n      try {\n        const usage = await result.usage;\n        const meta = JSON.stringify({\n          __meta: \"usage\",\n          promptTokens: usage.inputTokens,\n          completionTokens: usage.outputTokens,\n          totalTokens: usage.totalTokens,\n          cachedTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,\n          cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,\n        });\n        controller.enqueue(encoder.encode(`\\n${meta}\\n`));\n      } catch {\n        // Usage not available\n      }\n      controller.close();\n    },\n  });\n\n  return new Response(stream, {\n    headers: { \"Content-Type\": \"text/plain; charset=utf-8\" },\n  });\n}\n"
  },
  {
    "path": "apps/web/app/api/search/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { getSearchIndex } from \"@/lib/search-index\";\n\nexport async function GET(req: NextRequest) {\n  const q = req.nextUrl.searchParams.get(\"q\")?.trim().toLowerCase();\n\n  if (!q) {\n    return NextResponse.json({ results: [] });\n  }\n\n  const index = await getSearchIndex();\n  const terms = q.split(/\\s+/).filter(Boolean);\n\n  const results = index\n    .map((entry) => {\n      const titleLower = entry.title.toLowerCase();\n      const contentLower = entry.content.toLowerCase();\n\n      const titleMatch = terms.every((t) => titleLower.includes(t));\n      const contentMatch = terms.every((t) => contentLower.includes(t));\n\n      if (!titleMatch && !contentMatch) return null;\n\n      let snippet = \"\";\n      if (contentMatch) {\n        const firstTermIdx = Math.min(\n          ...terms.map((t) => {\n            const idx = contentLower.indexOf(t);\n            return idx === -1 ? Infinity : idx;\n          }),\n        );\n        if (firstTermIdx !== Infinity) {\n          const start = Math.max(0, firstTermIdx - 40);\n          const end = Math.min(entry.content.length, firstTermIdx + 120);\n          snippet =\n            (start > 0 ? \"...\" : \"\") +\n            entry.content.slice(start, end).replace(/\\n/g, \" \") +\n            (end < entry.content.length ? \"...\" : \"\");\n        }\n      }\n\n      return {\n        title: entry.title,\n        href: entry.href,\n        section: entry.section,\n        snippet,\n        score: titleMatch ? 2 : 1,\n      };\n    })\n    .filter(\n      (\n        r,\n      ): r is {\n        title: string;\n        href: string;\n        section: string;\n        snippet: string;\n        score: number;\n      } => r !== null,\n    )\n    .sort((a, b) => b.score - a.score)\n    .slice(0, 20)\n    .map(({ title, href, section, snippet }) => ({\n      title,\n      href,\n      section,\n      snippet,\n    }));\n\n  return NextResponse.json(\n    { results },\n    { headers: { \"Cache-Control\": \"public, max-age=60\" } },\n  );\n}\n"
  },
  {
    "path": "apps/web/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@source \"../node_modules/streamdown/dist/index.js\";\n\n@custom-variant dark (&:is(.dark *));\n\n:root {\n  --radius: 0.5rem;\n  --ds-gray-500: oklch(0.836 0 0);\n  /* Monochrome light theme */\n  --background: oklch(1.0 0 0);\n  --foreground: oklch(0.1 0 0);\n  --card: oklch(0.98 0 0);\n  --card-foreground: oklch(0.1 0 0);\n  --popover: oklch(0.98 0 0);\n  --popover-foreground: oklch(0.1 0 0);\n  --primary: oklch(0.1 0 0);\n  --primary-foreground: oklch(1.0 0 0);\n  --secondary: oklch(0.92 0 0);\n  --secondary-foreground: oklch(0.1 0 0);\n  --muted: oklch(0.92 0 0);\n  --muted-foreground: oklch(0.45 0 0);\n  --accent: oklch(0.92 0 0);\n  --accent-foreground: oklch(0.1 0 0);\n  --destructive: oklch(0.55 0.2 25);\n  --destructive-foreground: oklch(1.0 0 0);\n  --border: oklch(0.85 0 0);\n  --input: oklch(0.85 0 0);\n  --ring: oklch(0.6 0 0);\n  --chat-bg: oklch(0.95 0 0);\n}\n\n.dark {\n  --ds-gray-500: oklch(0.39 0 0);\n  /* Monochrome dark theme */\n  --background: oklch(0.0 0 0);\n  --foreground: oklch(0.98 0 0);\n  --card: oklch(0.08 0 0);\n  --card-foreground: oklch(0.98 0 0);\n  --popover: oklch(0.08 0 0);\n  --popover-foreground: oklch(0.98 0 0);\n  --primary: oklch(0.98 0 0);\n  --primary-foreground: oklch(0.0 0 0);\n  --secondary: oklch(0.15 0 0);\n  --secondary-foreground: oklch(0.98 0 0);\n  --muted: oklch(0.15 0 0);\n  --muted-foreground: oklch(0.6 0 0);\n  --accent: oklch(0.15 0 0);\n  --accent-foreground: oklch(0.98 0 0);\n  --destructive: oklch(0.65 0.2 25);\n  --destructive-foreground: oklch(0.98 0 0);\n  --border: oklch(0.25 0 0);\n  --input: oklch(0.25 0 0);\n  --ring: oklch(0.4 0 0);\n  --chat-bg: oklch(0.25 0 0);\n}\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  \n  body {\n    @apply bg-background text-foreground antialiased;\n    font-family: var(--font-geist-sans), system-ui, sans-serif;\n  }\n\n  ::selection {\n    @apply bg-foreground text-background;\n  }\n\n  code {\n    font-family: var(--font-geist-mono), ui-monospace, monospace;\n    @apply bg-secondary px-1.5 py-0.5 rounded text-sm;\n  }\n\n  pre {\n    font-family: var(--font-geist-mono), ui-monospace, monospace;\n    @apply overflow-x-auto text-sm leading-relaxed;\n  }\n\n  pre code {\n    @apply bg-transparent p-0;\n  }\n\n  /* Hide page scrollbar */\n  html {\n    scrollbar-width: none;\n  }\n\n  html::-webkit-scrollbar {\n    display: none;\n  }\n\n}\n\nbutton {\n  cursor: pointer;\n}\n\n/* Tool call shimmer animation */\n@keyframes tool-shimmer {\n  0% { opacity: 0.5; }\n  50% { opacity: 1; }\n  100% { opacity: 0.5; }\n}\n\n.animate-tool-shimmer {\n  animation: tool-shimmer 1.5s ease-in-out infinite;\n}\n\n/* Fix list rendering in chat content */\n.docs-chat-content ul,\n.docs-chat-content ol {\n  list-style-position: outside;\n  padding-left: 1.25em;\n}\n\n.docs-chat-content li > p {\n  display: inline;\n  margin: 0;\n}\n\n.docs-chat-content li {\n  margin-top: 0.5em;\n  margin-bottom: 0.5em;\n}\n\n/* MDX table styles — applies to both GFM pipe tables and raw HTML tables */\n.mdx-table th,\n.mdx-table td,\narticle table th,\narticle table td {\n  border: 1px solid var(--border);\n  padding: 0.75rem 1rem;\n  text-align: left;\n}\n\n.mdx-table th,\narticle table th {\n  font-weight: 600;\n  background-color: var(--muted);\n}\n\n.mdx-table td,\narticle table td {\n  color: var(--muted-foreground);\n}\n\narticle table {\n  width: 100%;\n  font-size: 0.875rem;\n  border-collapse: collapse;\n  margin: 1.5rem 0;\n}\n\n/* Shiki dual theme support */\n.shiki,\n.shiki span {\n  color: var(--shiki-light) !important;\n  background-color: var(--shiki-light-bg) !important;\n}\n\n.dark .shiki,\n.dark .shiki span {\n  color: var(--shiki-dark) !important;\n  background-color: var(--shiki-dark-bg) !important;\n}\n\n"
  },
  {
    "path": "apps/web/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport { GeistPixelSquare } from \"geist/font/pixel\";\nimport \"./globals.css\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { DocsChat } from \"@/components/docs-chat\";\nimport { Analytics } from \"@vercel/analytics/next\";\nimport { SpeedInsights } from \"@vercel/speed-insights/next\";\nimport { PAGE_TITLES } from \"@/lib/page-titles\";\nimport { cookies } from \"next/headers\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n});\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n});\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(\"https://json-render.dev\"),\n  title: {\n    default: `json-render | ${PAGE_TITLES[\"\"]}`,\n    template: \"%s | json-render\",\n  },\n  description:\n    \"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.\",\n  keywords: [\n    \"json-render\",\n    \"generative UI\",\n    \"AI UI generation\",\n    \"user-generated interfaces\",\n    \"React components\",\n    \"React Native\",\n    \"guardrails\",\n    \"structured output\",\n    \"dashboard builder\",\n  ],\n  authors: [{ name: \"Vercel Labs\" }],\n  creator: \"Vercel Labs\",\n  openGraph: {\n    type: \"website\",\n    locale: \"en_US\",\n    url: \"https://json-render.dev\",\n    siteName: \"json-render\",\n    title: \"json-render | The Generative UI Framework\",\n    description:\n      \"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.\",\n    images: [\n      {\n        url: \"/og\",\n        width: 1200,\n        height: 630,\n        alt: \"json-render - The Generative UI Framework\",\n      },\n    ],\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    title: \"json-render | The Generative UI Framework\",\n    description:\n      \"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.\",\n    images: [\"/og\"],\n  },\n  robots: {\n    index: true,\n    follow: true,\n  },\n  icons: {\n    icon: \"/favicon.ico\",\n  },\n};\n\nexport default async function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const cookieStore = await cookies();\n  const chatOpen = cookieStore.get(\"docs-chat-open\")?.value === \"true\";\n  const chatWidth = Number(cookieStore.get(\"docs-chat-width\")?.value) || 400;\n\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <head>\n        {chatOpen && (\n          <style\n            dangerouslySetInnerHTML={{\n              __html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,\n            }}\n          />\n        )}\n      </head>\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}\n      >\n        <ThemeProvider>\n          {children}\n          <DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />\n        </ThemeProvider>\n        <Analytics />\n        <SpeedInsights />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/not-found.tsx",
    "content": "import Link from \"next/link\";\nimport { Header } from \"@/components/header\";\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex min-h-screen flex-col\">\n      <Header />\n      <main className=\"flex flex-1 flex-col items-center justify-center gap-4 px-4 text-center\">\n        <h1 className=\"text-6xl font-bold tracking-tight\">404</h1>\n        <p className=\"text-lg text-muted-foreground\">\n          This page could not be found.\n        </p>\n        <Link\n          href=\"/\"\n          className=\"mt-2 inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors\"\n        >\n          Go home\n        </Link>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/og/[...slug]/route.tsx",
    "content": "import { NextResponse } from \"next/server\";\nimport { getPageTitle, renderOgImage } from \"../og-image\";\n\nexport async function GET(\n  _request: Request,\n  { params }: { params: Promise<{ slug: string[] }> },\n) {\n  const { slug } = await params;\n  const title = getPageTitle(slug.join(\"/\"));\n\n  if (!title) {\n    return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n  }\n\n  return renderOgImage(title);\n}\n"
  },
  {
    "path": "apps/web/app/og/og-image.tsx",
    "content": "import { ImageResponse } from \"next/og\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport { getPageTitle } from \"@/lib/page-titles\";\n\n// Cache font data in memory after first load\nlet fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;\n\nasync function loadFonts() {\n  if (fontCache) return fontCache;\n  const [geistRegular, geistPixelSquare] = await Promise.all([\n    readFile(join(process.cwd(), \"public/Geist-Regular.ttf\")),\n    readFile(join(process.cwd(), \"public/GeistPixel-Square.ttf\")),\n  ]);\n  fontCache = { geistRegular, geistPixelSquare };\n  return fontCache;\n}\n\nexport async function renderOgImage(title: string) {\n  const { geistRegular, geistPixelSquare } = await loadFonts();\n\n  return new ImageResponse(\n    <div\n      style={{\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        backgroundColor: \"black\",\n        padding: \"60px 80px\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: \"16px\",\n        }}\n      >\n        <svg width=\"36\" height=\"36\" viewBox=\"0 0 16 16\" fill=\"white\">\n          <path fillRule=\"evenodd\" clipRule=\"evenodd\" d=\"M8 1L16 15H0L8 1Z\" />\n        </svg>\n        <span\n          style={{\n            fontSize: 36,\n            color: \"#666\",\n            fontFamily: \"Geist\",\n            fontWeight: 400,\n          }}\n        >\n          /\n        </span>\n        <span\n          style={{\n            fontSize: 36,\n            fontFamily: \"Geist Pixel Square\",\n            fontWeight: 500,\n            color: \"white\",\n          }}\n        >\n          json-render\n        </span>\n      </div>\n\n      <div\n        style={{\n          display: \"flex\",\n          flex: 1,\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        {title.split(\"\\n\").map((line, i) => (\n          <span\n            key={i}\n            style={{\n              fontSize: 72,\n              fontFamily: \"Geist\",\n              fontWeight: 400,\n              color: \"white\",\n              letterSpacing: \"-0.02em\",\n              textAlign: \"center\",\n              lineHeight: 1.2,\n            }}\n          >\n            {line}\n          </span>\n        ))}\n      </div>\n    </div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [\n        {\n          name: \"Geist\",\n          data: geistRegular.buffer as ArrayBuffer,\n          style: \"normal\",\n          weight: 400,\n        },\n        {\n          name: \"Geist Pixel Square\",\n          data: geistPixelSquare.buffer as ArrayBuffer,\n          style: \"normal\",\n          weight: 500,\n        },\n      ],\n    },\n  );\n}\n"
  },
  {
    "path": "apps/web/app/og/route.tsx",
    "content": "import { getPageTitle, renderOgImage } from \"./og-image\";\n\nexport async function GET() {\n  const title = getPageTitle(\"\")!;\n  return renderOgImage(title);\n}\n"
  },
  {
    "path": "apps/web/app/playground/layout.tsx",
    "content": "export default function PlaygroundLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <div className=\"h-dvh flex flex-col overflow-hidden\">{children}</div>;\n}\n"
  },
  {
    "path": "apps/web/app/playground/page.tsx",
    "content": "import { Playground } from \"@/components/playground\";\nimport { pageMetadata } from \"@/lib/page-metadata\";\n\nexport const metadata = pageMetadata(\"playground\");\n\nexport default function PlaygroundPage() {\n  return <Playground />;\n}\n"
  },
  {
    "path": "apps/web/components/code-block.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createHighlighter, type Highlighter } from \"shiki\";\nimport { CopyButton } from \"./copy-button\";\n\nconst vercelDarkTheme = {\n  name: \"vercel-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#666666\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#7928CA\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#888888\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n  ],\n};\n\nconst vercelLightTheme = {\n  name: \"vercel-light\",\n  type: \"light\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#171717\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#6e56cf\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n  ],\n};\n\n// Preload highlighter on module load\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\nfunction getHighlighter() {\n  if (!highlighterPromise) {\n    highlighterPromise = createHighlighter({\n      themes: [vercelLightTheme, vercelDarkTheme],\n      langs: [\"json\", \"tsx\", \"typescript\", \"yaml\"],\n    });\n  }\n  return highlighterPromise;\n}\n\n// Start loading immediately when module is imported\nif (typeof window !== \"undefined\") {\n  getHighlighter();\n}\n\ninterface CodeBlockProps {\n  code: string;\n  lang: \"json\" | \"tsx\" | \"typescript\" | \"yaml\";\n  fillHeight?: boolean;\n  hideCopyButton?: boolean;\n}\n\nexport function CodeBlock({\n  code,\n  lang,\n  fillHeight,\n  hideCopyButton,\n}: CodeBlockProps) {\n  const [html, setHtml] = useState<string>(\"\");\n\n  useEffect(() => {\n    getHighlighter().then((highlighter) => {\n      setHtml(\n        highlighter.codeToHtml(code, {\n          lang,\n          themes: {\n            light: \"vercel-light\",\n            dark: \"vercel-dark\",\n          },\n          defaultColor: false,\n        }),\n      );\n    });\n  }, [code, lang]);\n\n  if (!html) {\n    return fillHeight ? <div className=\"p-3\" /> : null;\n  }\n\n  return (\n    <div className={`relative group ${fillHeight ? \"p-3\" : \"\"}`}>\n      {!hideCopyButton && (\n        <div className=\"float-right sticky top-3 z-10 ml-2\">\n          <CopyButton\n            text={code}\n            className=\"opacity-0 group-hover:opacity-100 text-neutral-400\"\n          />\n        </div>\n      )}\n      <div\n        className=\"text-[13px] leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-0! [&_pre]:m-0! [&_pre]:border-none! [&_pre]:rounded-none! [&_pre]:text-[13px]! [&_pre]:overflow-visible! [&_code]:bg-transparent! [&_code]:p-0! [&_code]:rounded-none! [&_code]:text-[13px]!\"\n        dangerouslySetInnerHTML={{ __html: html }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/code-tabs.tsx",
    "content": "\"use client\";\n\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { CopyButton } from \"./copy-button\";\n\ninterface CodeTabsProps {\n  tabs: {\n    label: string;\n    value: string;\n    code: string;\n    html: string;\n  }[];\n  defaultValue?: string;\n}\n\nexport function CodeTabs({ tabs, defaultValue }: CodeTabsProps) {\n  const defaultTab = defaultValue ?? tabs[0]?.value;\n\n  return (\n    <div className=\"my-6 rounded-lg border border-border bg-neutral-100 dark:bg-[#0a0a0a] text-sm font-mono overflow-hidden\">\n      <Tabs defaultValue={defaultTab} className=\"gap-0\">\n        <div className=\"flex items-center justify-between border-b border-border px-4 py-2\">\n          <TabsList className=\"h-7 bg-transparent p-0 gap-2\">\n            {tabs.map((tab) => (\n              <TabsTrigger\n                key={tab.value}\n                value={tab.value}\n                className=\"h-6 px-2 text-xs data-[state=active]:bg-secondary data-[state=active]:shadow-none rounded\"\n              >\n                {tab.label}\n              </TabsTrigger>\n            ))}\n          </TabsList>\n        </div>\n        {tabs.map((tab) => (\n          <TabsContent\n            key={tab.value}\n            value={tab.value}\n            className=\"relative group mt-0\"\n          >\n            <div className=\"absolute top-3 right-3 z-10\">\n              <CopyButton\n                text={tab.code}\n                className=\"opacity-0 group-hover:opacity-100 text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-[#0a0a0a]\"\n              />\n            </div>\n            <div\n              className=\"overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!\"\n              dangerouslySetInnerHTML={{ __html: tab.html }}\n            />\n          </TabsContent>\n        ))}\n      </Tabs>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/code.tsx",
    "content": "import { codeToHtml } from \"shiki\";\nimport { CopyButton } from \"./copy-button\";\nimport { ExpandableCode } from \"./expandable-code\";\n\nconst vercelDarkTheme = {\n  name: \"vercel-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#666666\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#7928CA\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#888888\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n  ],\n};\n\nconst vercelLightTheme = {\n  name: \"vercel-light\",\n  type: \"light\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#171717\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#6e56cf\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n  ],\n};\n\ninterface CodeProps {\n  children: string;\n  lang?: \"json\" | \"tsx\" | \"typescript\" | \"bash\" | \"javascript\";\n}\n\nexport async function Code({ children, lang = \"typescript\" }: CodeProps) {\n  const html = await codeToHtml(children.trim(), {\n    lang,\n    themes: {\n      light: vercelLightTheme,\n      dark: vercelDarkTheme,\n    },\n    defaultColor: false,\n  });\n\n  return (\n    <div className=\"group relative my-6 rounded-lg border border-border bg-neutral-100 dark:bg-[#0a0a0a] text-sm font-mono overflow-hidden max-w-full\">\n      <div className=\"absolute top-3 right-3 z-10\">\n        <CopyButton\n          text={children.trim()}\n          className=\"opacity-0 group-hover:opacity-100 text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-[#0a0a0a]\"\n        />\n      </div>\n      <ExpandableCode>\n        <div\n          className=\"overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!\"\n          dangerouslySetInnerHTML={{ __html: html }}\n        />\n      </ExpandableCode>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/copy-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\n\ninterface CopyButtonProps {\n  text: string;\n  className?: string;\n}\n\nexport function CopyButton({ text, className = \"\" }: CopyButtonProps) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async () => {\n    await navigator.clipboard.writeText(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <button\n      onClick={handleCopy}\n      className={`p-1.5 rounded hover:bg-black/10 dark:hover:bg-white/10 transition-colors ${className}`}\n      aria-label=\"Copy code\"\n    >\n      {copied ? (\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <polyline points=\"20 6 9 17 4 12\" />\n        </svg>\n      ) : (\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n          <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/copy-page-button.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\n\nexport function CopyPageButton() {\n  const pathname = usePathname();\n  const [state, setState] = useState<\"idle\" | \"loading\" | \"copied\">(\"idle\");\n\n  const handleCopy = async () => {\n    setState(\"loading\");\n    try {\n      const response = await fetch(\n        `/api/docs-markdown?path=${encodeURIComponent(pathname)}`,\n      );\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch markdown\");\n      }\n      const markdown = await response.text();\n      await navigator.clipboard.writeText(markdown);\n      setState(\"copied\");\n      setTimeout(() => setState(\"idle\"), 2000);\n    } catch {\n      setState(\"idle\");\n    }\n  };\n\n  return (\n    <button\n      onClick={handleCopy}\n      disabled={state === \"loading\"}\n      className=\"flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors disabled:opacity-50\"\n      aria-label=\"Copy page as Markdown\"\n    >\n      {state === \"copied\" ? (\n        <>\n          <svg\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <polyline points=\"20 6 9 17 4 12\" />\n          </svg>\n          Copied\n        </>\n      ) : (\n        <>\n          <svg\n            width=\"14\"\n            height=\"14\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n            <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n          </svg>\n          Copy Page\n        </>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/demo.tsx",
    "content": "\"use client\";\n\nimport React, {\n  useEffect,\n  useState,\n  useCallback,\n  useRef,\n  useMemo,\n} from \"react\";\nimport { useUIStream } from \"@json-render/react\";\nimport type { Spec } from \"@json-render/core\";\nimport { collectUsedComponents, serializeProps } from \"@json-render/codegen\";\nimport { toast } from \"sonner\";\nimport { CodeBlock } from \"./code-block\";\nimport { CopyButton } from \"./copy-button\";\nimport { Toaster } from \"./ui/sonner\";\nimport { PlaygroundRenderer } from \"@/lib/render/renderer\";\nimport { playgroundCatalog } from \"@/lib/render/catalog\";\nimport { buildCatalogDisplayData } from \"@/lib/render/catalog-display\";\n\nconst SIMULATION_PROMPT = \"Create a contact form with name, email, and message\";\n\ninterface SimulationStage {\n  tree: Spec;\n  stream: string;\n}\n\n// Shared state & element definitions for the progressive simulation stages.\nconst FORM_STATE = { form: { name: \"\", email: \"\", message: \"\" } };\n\nconst NAME_INPUT = {\n  type: \"Input\",\n  props: {\n    label: \"Name\",\n    name: \"name\",\n    statePath: \"/form/name\",\n    checks: [{ type: \"required\", message: \"Name is required\" }],\n  },\n} as const;\n\nconst EMAIL_INPUT = {\n  type: \"Input\",\n  props: {\n    label: \"Email\",\n    name: \"email\",\n    type: \"email\",\n    statePath: \"/form/email\",\n    checks: [\n      { type: \"required\", message: \"Email is required\" },\n      { type: \"email\", message: \"Please enter a valid email\" },\n    ],\n  },\n} as const;\n\nconst MESSAGE_INPUT = {\n  type: \"Textarea\",\n  props: {\n    label: \"Message\",\n    name: \"message\",\n    statePath: \"/form/message\",\n    checks: [{ type: \"required\", message: \"Message is required\" }],\n  },\n} as const;\n\nconst SUBMIT_BUTTON = {\n  type: \"Button\",\n  props: { label: \"Send Message\", variant: \"primary\" },\n  on: { press: { action: \"formSubmit\" } },\n} as const;\n\nconst SIMULATION_STAGES: SimulationStage[] = [\n  {\n    tree: {\n      root: \"card\",\n      state: FORM_STATE,\n      elements: {\n        card: {\n          type: \"Card\",\n          props: { title: \"Contact Us\", maxWidth: \"md\" },\n          children: [],\n        },\n      },\n    },\n    stream: '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"card\"}',\n  },\n  {\n    tree: {\n      root: \"card\",\n      state: FORM_STATE,\n      elements: {\n        card: {\n          type: \"Card\",\n          props: { title: \"Contact Us\", maxWidth: \"md\" },\n          children: [\"name\"],\n        },\n        name: NAME_INPUT,\n      },\n    },\n    stream:\n      '{\"op\":\"add\",\"path\":\"/elements/name\",\"value\":{\"type\":\"Input\",\"props\":{\"label\":\"Name\",\"name\":\"name\",\"statePath\":\"/form/name\",\"checks\":[{\"type\":\"required\",\"message\":\"Name is required\"}]}}}',\n  },\n  {\n    tree: {\n      root: \"card\",\n      state: FORM_STATE,\n      elements: {\n        card: {\n          type: \"Card\",\n          props: { title: \"Contact Us\", maxWidth: \"md\" },\n          children: [\"name\", \"email\"],\n        },\n        name: NAME_INPUT,\n        email: EMAIL_INPUT,\n      },\n    },\n    stream:\n      '{\"op\":\"add\",\"path\":\"/elements/email\",\"value\":{\"type\":\"Input\",\"props\":{\"label\":\"Email\",\"name\":\"email\",\"type\":\"email\",\"statePath\":\"/form/email\",\"checks\":[{\"type\":\"required\",\"message\":\"Email is required\"},{\"type\":\"email\",\"message\":\"Please enter a valid email\"}]}}}',\n  },\n  {\n    tree: {\n      root: \"card\",\n      state: FORM_STATE,\n      elements: {\n        card: {\n          type: \"Card\",\n          props: { title: \"Contact Us\", maxWidth: \"md\" },\n          children: [\"name\", \"email\", \"message\"],\n        },\n        name: NAME_INPUT,\n        email: EMAIL_INPUT,\n        message: MESSAGE_INPUT,\n      },\n    },\n    stream:\n      '{\"op\":\"add\",\"path\":\"/elements/message\",\"value\":{\"type\":\"Textarea\",\"props\":{\"label\":\"Message\",\"name\":\"message\",\"statePath\":\"/form/message\",\"checks\":[{\"type\":\"required\",\"message\":\"Message is required\"}]}}}',\n  },\n  {\n    tree: {\n      root: \"card\",\n      state: FORM_STATE,\n      elements: {\n        card: {\n          type: \"Card\",\n          props: { title: \"Contact Us\", maxWidth: \"md\" },\n          children: [\"name\", \"email\", \"message\", \"submit\"],\n        },\n        name: NAME_INPUT,\n        email: EMAIL_INPUT,\n        message: MESSAGE_INPUT,\n        submit: SUBMIT_BUTTON,\n      },\n    },\n    stream:\n      '{\"op\":\"add\",\"path\":\"/elements/submit\",\"value\":{\"type\":\"Button\",\"props\":{\"label\":\"Send Message\",\"variant\":\"primary\"},\"on\":{\"press\":{\"action\":\"formSubmit\"}}}}',\n  },\n];\n\ntype Mode = \"simulation\" | \"interactive\";\ntype Phase = \"typing\" | \"streaming\" | \"complete\";\ntype Tab = \"stream\" | \"json\" | \"nested\" | \"catalog\";\ntype RenderView = \"dynamic\" | \"static\";\n\ninterface DemoProps {\n  fullscreen?: boolean;\n  skipSimulation?: boolean;\n}\n\n/**\n * Convert a flat Spec into a nested tree structure that is easier for humans\n * to read. Children keys are resolved recursively into inline objects.\n */\nfunction specToNested(spec: Spec): Record<string, unknown> {\n  function resolve(key: string): Record<string, unknown> {\n    const el = spec.elements[key];\n    if (!el) return { _key: key, _missing: true };\n\n    const node: Record<string, unknown> = { type: el.type };\n\n    if (el.props && Object.keys(el.props).length > 0) {\n      node.props = el.props;\n    }\n\n    if (el.visible !== undefined) {\n      node.visible = el.visible;\n    }\n\n    if (el.on && Object.keys(el.on).length > 0) {\n      node.on = el.on;\n    }\n\n    if (el.repeat) {\n      node.repeat = el.repeat;\n    }\n\n    if (el.children && el.children.length > 0) {\n      node.children = el.children.map(resolve);\n    }\n\n    return node;\n  }\n\n  const result: Record<string, unknown> = {};\n\n  if (spec.state && Object.keys(spec.state).length > 0) {\n    result.state = spec.state;\n  }\n\n  result.elements = resolve(spec.root);\n\n  return result;\n}\n\nconst EXAMPLE_PROMPTS = [\n  \"Create a login form with email and password\",\n  \"Build a feedback form with rating stars\",\n  \"Design a contact card with avatar\",\n  \"Make a settings panel with toggles\",\n];\n\nexport function Demo({\n  fullscreen = false,\n  skipSimulation = false,\n}: DemoProps) {\n  const [mode, setMode] = useState<Mode>(\n    skipSimulation ? \"interactive\" : \"simulation\",\n  );\n  const [phase, setPhase] = useState<Phase>(\n    skipSimulation ? \"complete\" : \"typing\",\n  );\n  const [typedPrompt, setTypedPrompt] = useState(\"\");\n  const [userPrompt, setUserPrompt] = useState(\"\");\n  const [stageIndex, setStageIndex] = useState(-1);\n  const [streamLines, setStreamLines] = useState<string[]>([]);\n  const [activeTab, setActiveTab] = useState<Tab>(\"json\");\n  const [renderView, setRenderView] = useState<RenderView>(\"dynamic\");\n  const [simulationTree, setSimulationTree] = useState<Spec | null>(null);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n  const [showExportModal, setShowExportModal] = useState(false);\n  const [selectedExportFile, setSelectedExportFile] = useState<string | null>(\n    null,\n  );\n  const [showMobileFileTree, setShowMobileFileTree] = useState(false);\n  const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(\n    new Set(),\n  );\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [catalogSection, setCatalogSection] = useState<\n    \"components\" | \"actions\"\n  >(\"components\");\n\n  // Catalog data for the catalog tab\n  const catalogData = useMemo(\n    () => buildCatalogDisplayData(playgroundCatalog.data),\n    [],\n  );\n\n  // Disable body scroll when any modal is open\n  useEffect(() => {\n    if (isFullscreen || showExportModal) {\n      document.body.style.overflow = \"hidden\";\n    } else {\n      document.body.style.overflow = \"\";\n    }\n    return () => {\n      document.body.style.overflow = \"\";\n    };\n  }, [isFullscreen, showExportModal]);\n\n  // Use the library's useUIStream hook for real API calls\n  const {\n    spec: apiSpec,\n    isStreaming,\n    send,\n    clear,\n    rawLines: apiRawLines,\n  } = useUIStream({\n    api: \"/api/generate\",\n    onError: (err: Error) => {\n      console.error(\"Generation error:\", err);\n      toast.error(err.message || \"Generation failed. Please try again.\");\n    },\n  } as Parameters<typeof useUIStream>[0]);\n\n  const currentSimulationStage =\n    stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;\n\n  // Determine which tree to display - keep simulation tree until new API response\n  const currentTree =\n    mode === \"simulation\"\n      ? currentSimulationStage?.tree || simulationTree\n      : apiSpec || simulationTree;\n\n  const stopGeneration = useCallback(() => {\n    if (mode === \"simulation\") {\n      setMode(\"interactive\");\n      setPhase(\"complete\");\n      setTypedPrompt(SIMULATION_PROMPT);\n      setUserPrompt(\"\");\n    }\n    clear();\n  }, [mode, clear]);\n\n  // Typing effect for simulation\n  useEffect(() => {\n    if (mode !== \"simulation\" || phase !== \"typing\") return;\n\n    let i = 0;\n    const interval = setInterval(() => {\n      if (i < SIMULATION_PROMPT.length) {\n        setTypedPrompt(SIMULATION_PROMPT.slice(0, i + 1));\n        i++;\n      } else {\n        clearInterval(interval);\n        setTimeout(() => setPhase(\"streaming\"), 500);\n      }\n    }, 20);\n\n    return () => clearInterval(interval);\n  }, [mode, phase]);\n\n  // Streaming effect for simulation\n  useEffect(() => {\n    if (mode !== \"simulation\" || phase !== \"streaming\") return;\n\n    let i = 0;\n    const interval = setInterval(() => {\n      if (i < SIMULATION_STAGES.length) {\n        const stage = SIMULATION_STAGES[i];\n        if (stage) {\n          setStageIndex(i);\n          setStreamLines((prev) => [...prev, stage.stream]);\n          setSimulationTree(stage.tree);\n        }\n        i++;\n      } else {\n        clearInterval(interval);\n        setTimeout(() => {\n          setPhase(\"complete\");\n          setMode(\"interactive\");\n          setUserPrompt(\"\");\n        }, 500);\n      }\n    }, 600);\n\n    return () => clearInterval(interval);\n  }, [mode, phase]);\n\n  // Track stream lines from real API (use raw JSONL patch lines)\n  useEffect(() => {\n    if (mode === \"interactive\" && apiRawLines.length > 0) {\n      setStreamLines(apiRawLines);\n    }\n  }, [mode, apiRawLines]);\n\n  const handleSubmit = useCallback(async () => {\n    if (!userPrompt.trim() || isStreaming) return;\n    setStreamLines([]);\n    await send(userPrompt);\n  }, [userPrompt, isStreaming, send]);\n\n  const jsonCode = currentTree\n    ? JSON.stringify(currentTree, null, 2)\n    : \"// waiting...\";\n\n  const nestedCode = useMemo(() => {\n    if (!currentTree || !currentTree.root) return \"// waiting...\";\n    return JSON.stringify(specToNested(currentTree), null, 2);\n  }, [currentTree]);\n\n  // Generate all export files for Next.js project\n  const exportedFiles = useMemo(() => {\n    if (!currentTree || !currentTree.root) {\n      return [];\n    }\n\n    const tree = currentTree;\n    const components = collectUsedComponents(tree);\n    const files: { path: string; content: string }[] = [];\n\n    // Helper to generate JSX\n    function generateJSX(key: string, indent: number): string {\n      const element = tree.elements[key];\n      if (!element) return \"\";\n\n      const spaces = \"  \".repeat(indent);\n      const componentName = element.type;\n\n      const propsObj: Record<string, unknown> = {};\n      for (const [k, v] of Object.entries(element.props)) {\n        if (v !== null && v !== undefined) {\n          propsObj[k] = v;\n        }\n      }\n\n      const propsStr = serializeProps(propsObj);\n      const hasChildren = element.children && element.children.length > 0;\n\n      if (!hasChildren) {\n        return propsStr\n          ? `${spaces}<${componentName} ${propsStr} />`\n          : `${spaces}<${componentName} />`;\n      }\n\n      const lines: string[] = [];\n      lines.push(\n        propsStr\n          ? `${spaces}<${componentName} ${propsStr}>`\n          : `${spaces}<${componentName}>`,\n      );\n\n      for (const childKey of element.children!) {\n        lines.push(generateJSX(childKey, indent + 1));\n      }\n\n      lines.push(`${spaces}</${componentName}>`);\n      return lines.join(\"\\n\");\n    }\n\n    // 1. package.json\n    files.push({\n      path: \"package.json\",\n      content: JSON.stringify(\n        {\n          name: \"generated-app\",\n          version: \"0.1.0\",\n          private: true,\n          scripts: {\n            dev: \"next dev\",\n            build: \"next build\",\n            start: \"next start\",\n          },\n          dependencies: {\n            next: \"^16.1.3\",\n            react: \"^19.2.3\",\n            \"react-dom\": \"^19.2.3\",\n          },\n          devDependencies: {\n            \"@types/node\": \"^25.0.9\",\n            \"@types/react\": \"^19.2.8\",\n            typescript: \"^5.9.3\",\n          },\n        },\n        null,\n        2,\n      ),\n    });\n\n    // 2. tsconfig.json\n    files.push({\n      path: \"tsconfig.json\",\n      content: JSON.stringify(\n        {\n          compilerOptions: {\n            target: \"ES2017\",\n            lib: [\"dom\", \"dom.iterable\", \"esnext\"],\n            allowJs: true,\n            skipLibCheck: true,\n            strict: true,\n            noEmit: true,\n            esModuleInterop: true,\n            module: \"esnext\",\n            moduleResolution: \"bundler\",\n            resolveJsonModule: true,\n            isolatedModules: true,\n            jsx: \"preserve\",\n            incremental: true,\n            plugins: [{ name: \"next\" }],\n            paths: { \"@/*\": [\"./*\"] },\n          },\n          include: [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n          exclude: [\"node_modules\"],\n        },\n        null,\n        2,\n      ),\n    });\n\n    // 3. next.config.js\n    files.push({\n      path: \"next.config.js\",\n      content: `/** @type {import('next').NextConfig} */\nmodule.exports = {\n  reactStrictMode: true,\n};\n`,\n    });\n\n    // 4. app/globals.css\n    files.push({\n      path: \"app/globals.css\",\n      content: `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --background: #ffffff;\n  --foreground: #171717;\n  --border: #e5e5e5;\n  --muted-foreground: #737373;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n    --border: #262626;\n    --muted-foreground: #a3a3a3;\n  }\n}\n\nbody {\n  background: var(--background);\n  color: var(--foreground);\n  font-family: system-ui, sans-serif;\n}\n`,\n    });\n\n    // 5. tailwind.config.js\n    files.push({\n      path: \"tailwind.config.js\",\n      content: `/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./app/**/*.{ts,tsx}\", \"./components/**/*.{ts,tsx}\"],\n  theme: {\n    extend: {\n      colors: {\n        background: \"var(--background)\",\n        foreground: \"var(--foreground)\",\n        border: \"var(--border)\",\n        \"muted-foreground\": \"var(--muted-foreground)\",\n      },\n    },\n  },\n  plugins: [],\n};\n`,\n    });\n\n    // 6. app/layout.tsx\n    files.push({\n      path: \"app/layout.tsx\",\n      content: `import type { Metadata } from \"next\";\nimport \"./globals.css\";\n\nexport const metadata: Metadata = {\n  title: \"Generated App\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}\n`,\n    });\n\n    // 7. Component files\n    const componentTemplates: Record<string, string> = {\n      Card: `\"use client\";\n\nimport { ReactNode } from \"react\";\n\ninterface CardProps {\n  title?: string;\n  description?: string;\n  maxWidth?: \"sm\" | \"md\" | \"lg\";\n  children?: ReactNode;\n}\n\nexport function Card({ title, description, maxWidth, children }: CardProps) {\n  const widthClass = maxWidth === \"sm\" ? \"max-w-xs\" : maxWidth === \"md\" ? \"max-w-sm\" : maxWidth === \"lg\" ? \"max-w-md\" : \"w-full\";\n  \n  return (\n    <div className={\\`border border-border rounded-lg p-4 bg-background \\${widthClass}\\`}>\n      {title && <div className=\"font-semibold text-sm mb-1\">{title}</div>}\n      {description && <div className=\"text-xs text-muted-foreground mb-2\">{description}</div>}\n      <div className=\"space-y-3\">{children}</div>\n    </div>\n  );\n}\n`,\n      Input: `\"use client\";\n\ninterface InputProps {\n  label?: string;\n  name?: string;\n  type?: string;\n  placeholder?: string;\n}\n\nexport function Input({ label, name, type = \"text\", placeholder }: InputProps) {\n  return (\n    <div>\n      {label && <label className=\"text-xs text-muted-foreground block mb-1\">{label}</label>}\n      <input\n        type={type}\n        name={name}\n        placeholder={placeholder}\n        className=\"h-9 w-full bg-background border border-border rounded px-3 text-sm focus:outline-none focus:ring-2 focus:ring-foreground/20\"\n      />\n    </div>\n  );\n}\n`,\n      Textarea: `\"use client\";\n\ninterface TextareaProps {\n  label?: string;\n  name?: string;\n  placeholder?: string;\n  rows?: number;\n}\n\nexport function Textarea({ label, name, placeholder, rows = 3 }: TextareaProps) {\n  return (\n    <div>\n      {label && <label className=\"text-xs text-muted-foreground block mb-1\">{label}</label>}\n      <textarea\n        name={name}\n        placeholder={placeholder}\n        rows={rows}\n        className=\"w-full bg-background border border-border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-foreground/20 resize-none\"\n      />\n    </div>\n  );\n}\n`,\n      Button: `\"use client\";\n\ninterface ButtonProps {\n  label: string;\n  variant?: \"primary\" | \"secondary\" | \"outline\";\n  onClick?: () => void;\n}\n\nexport function Button({ label, variant = \"primary\", onClick }: ButtonProps) {\n  const baseClass = \"px-4 py-2 rounded text-sm font-medium transition-colors\";\n  const variantClass = variant === \"primary\" \n    ? \"bg-foreground text-background hover:bg-foreground/90\"\n    : variant === \"outline\"\n    ? \"border border-border hover:bg-border/50\"\n    : \"bg-border/50 hover:bg-border\";\n    \n  return (\n    <button onClick={onClick} className={\\`\\${baseClass} \\${variantClass}\\`}>\n      {label}\n    </button>\n  );\n}\n`,\n      Text: `\"use client\";\n\ninterface TextProps {\n  content: string;\n  variant?: \"body\" | \"caption\" | \"label\";\n}\n\nexport function Text({ content, variant = \"body\" }: TextProps) {\n  const sizeClass = variant === \"caption\" ? \"text-xs\" : variant === \"label\" ? \"text-sm font-medium\" : \"text-sm\";\n  return <p className={\\`\\${sizeClass} text-muted-foreground\\`}>{content}</p>;\n}\n`,\n      Heading: `\"use client\";\n\ninterface HeadingProps {\n  text: string;\n  level?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n}\n\nexport function Heading({ text, level = \"h2\" }: HeadingProps) {\n  const Tag = level;\n  const sizeClass = level === \"h1\" ? \"text-2xl\" : level === \"h2\" ? \"text-xl\" : level === \"h3\" ? \"text-lg\" : \"text-base\";\n  return <Tag className={\\`\\${sizeClass} font-semibold\\`}>{text}</Tag>;\n}\n`,\n      Stack: `\"use client\";\n\nimport { ReactNode } from \"react\";\n\ninterface StackProps {\n  direction?: \"horizontal\" | \"vertical\";\n  gap?: \"sm\" | \"md\" | \"lg\";\n  children?: ReactNode;\n}\n\nexport function Stack({ direction = \"vertical\", gap = \"md\", children }: StackProps) {\n  const gapClass = gap === \"sm\" ? \"gap-2\" : gap === \"lg\" ? \"gap-6\" : \"gap-4\";\n  const dirClass = direction === \"horizontal\" ? \"flex-row\" : \"flex-col\";\n  return <div className={\\`flex \\${dirClass} \\${gapClass}\\`}>{children}</div>;\n}\n`,\n      Grid: `\"use client\";\n\nimport { ReactNode } from \"react\";\n\ninterface GridProps {\n  columns?: number;\n  gap?: \"sm\" | \"md\" | \"lg\";\n  children?: ReactNode;\n}\n\nexport function Grid({ columns = 2, gap = \"md\", children }: GridProps) {\n  const gapClass = gap === \"sm\" ? \"gap-2\" : gap === \"lg\" ? \"gap-6\" : \"gap-4\";\n  return (\n    <div className={\\`grid \\${gapClass}\\`} style={{ gridTemplateColumns: \\`repeat(\\${columns}, 1fr)\\` }}>\n      {children}\n    </div>\n  );\n}\n`,\n      Select: `\"use client\";\n\ninterface SelectProps {\n  label?: string;\n  name?: string;\n  options?: Array<{ value: string; label: string }>;\n  placeholder?: string;\n}\n\nexport function Select({ label, name, options = [], placeholder }: SelectProps) {\n  return (\n    <div>\n      {label && <label className=\"text-xs text-muted-foreground block mb-1\">{label}</label>}\n      <select\n        name={name}\n        className=\"h-9 w-full bg-background border border-border rounded px-3 text-sm focus:outline-none focus:ring-2 focus:ring-foreground/20\"\n      >\n        {placeholder && <option value=\"\">{placeholder}</option>}\n        {options.map((opt) => (\n          <option key={opt.value} value={opt.value}>{opt.label}</option>\n        ))}\n      </select>\n    </div>\n  );\n}\n`,\n      Checkbox: `\"use client\";\n\ninterface CheckboxProps {\n  label?: string;\n  name?: string;\n  checked?: boolean;\n}\n\nexport function Checkbox({ label, name, checked }: CheckboxProps) {\n  return (\n    <label className=\"flex items-center gap-2 text-sm\">\n      <input type=\"checkbox\" name={name} defaultChecked={checked} className=\"rounded border-border\" />\n      {label}\n    </label>\n  );\n}\n`,\n      Radio: `\"use client\";\n\ninterface RadioProps {\n  label?: string;\n  name?: string;\n  options?: Array<{ value: string; label: string }>;\n}\n\nexport function Radio({ label, name, options = [] }: RadioProps) {\n  return (\n    <div>\n      {label && <div className=\"text-xs text-muted-foreground mb-1\">{label}</div>}\n      <div className=\"space-y-1\">\n        {options.map((opt) => (\n          <label key={opt.value} className=\"flex items-center gap-2 text-sm\">\n            <input type=\"radio\" name={name} value={opt.value} className=\"border-border\" />\n            {opt.label}\n          </label>\n        ))}\n      </div>\n    </div>\n  );\n}\n`,\n      Divider: `\"use client\";\n\nexport function Divider() {\n  return <hr className=\"border-border my-4\" />;\n}\n`,\n      Badge: `\"use client\";\n\ninterface BadgeProps {\n  text: string;\n  variant?: \"default\" | \"success\" | \"warning\" | \"error\";\n}\n\nexport function Badge({ text, variant = \"default\" }: BadgeProps) {\n  const colorClass = variant === \"success\" ? \"bg-green-100 text-green-800\" \n    : variant === \"warning\" ? \"bg-yellow-100 text-yellow-800\"\n    : variant === \"error\" ? \"bg-red-100 text-red-800\"\n    : \"bg-border text-foreground\";\n  return <span className={\\`px-2 py-0.5 rounded text-xs \\${colorClass}\\`}>{text}</span>;\n}\n`,\n      Switch: `\"use client\";\n\ninterface SwitchProps {\n  label?: string;\n  name?: string;\n  checked?: boolean;\n}\n\nexport function Switch({ label, name, checked }: SwitchProps) {\n  return (\n    <label className=\"flex items-center justify-between gap-2 text-sm\">\n      {label}\n      <input type=\"checkbox\" name={name} defaultChecked={checked} className=\"sr-only peer\" />\n      <div className=\"w-9 h-5 bg-border rounded-full peer-checked:bg-foreground transition-colors relative after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:w-4 after:h-4 after:bg-background after:rounded-full after:transition-transform peer-checked:after:translate-x-4\" />\n    </label>\n  );\n}\n`,\n      Rating: `\"use client\";\n\ninterface RatingProps {\n  label?: string;\n  value?: number;\n  max?: number;\n}\n\nexport function Rating({ label, value = 0, max = 5 }: RatingProps) {\n  return (\n    <div>\n      {label && <div className=\"text-xs text-muted-foreground mb-1\">{label}</div>}\n      <div className=\"flex gap-1\">\n        {Array.from({ length: max }).map((_, i) => (\n          <span key={i} className={\\`text-lg \\${i < value ? \"text-yellow-400\" : \"text-border\"}\\`}>★</span>\n        ))}\n      </div>\n    </div>\n  );\n}\n`,\n      Form: `\"use client\";\n\nimport { ReactNode } from \"react\";\n\ninterface FormProps {\n  children?: ReactNode;\n}\n\nexport function Form({ children }: FormProps) {\n  return <form className=\"space-y-4\" onSubmit={(e) => e.preventDefault()}>{children}</form>;\n}\n`,\n    };\n\n    // Add component files\n    for (const comp of components) {\n      const template = componentTemplates[comp];\n      if (template) {\n        files.push({\n          path: `components/ui/${comp.toLowerCase()}.tsx`,\n          content: template,\n        });\n      }\n    }\n\n    // 8. components/ui/index.ts\n    const indexExports = Array.from(components)\n      .filter((c) => componentTemplates[c])\n      .map((c) => `export { ${c} } from \"./${c.toLowerCase()}\";`)\n      .join(\"\\n\");\n    files.push({\n      path: \"components/ui/index.ts\",\n      content: indexExports + \"\\n\",\n    });\n\n    // 9. app/page.tsx\n    const jsx = generateJSX(tree.root, 2);\n    const imports = Array.from(components)\n      .filter((c) => componentTemplates[c])\n      .sort()\n      .join(\", \");\n    files.push({\n      path: \"app/page.tsx\",\n      content: `\"use client\";\n\nimport { ${imports} } from \"@/components/ui\";\n\nexport default function Page() {\n  return (\n    <div className=\"min-h-screen p-8 flex items-center justify-center\">\n${jsx}\n    </div>\n  );\n}\n`,\n    });\n\n    // 10. README.md\n    files.push({\n      path: \"README.md\",\n      content: `# Generated App\n\nThis app was generated from a json-render UI tree.\n\n## Getting Started\n\n\\`\\`\\`bash\nnpm install\nnpm run dev\n\\`\\`\\`\n\nOpen [http://localhost:3000](http://localhost:3000) to view.\n`,\n    });\n\n    return files;\n  }, [currentTree]);\n\n  // Reset state when export modal closes\n  useEffect(() => {\n    if (!showExportModal) {\n      setCollapsedFolders(new Set());\n      setSelectedExportFile(null);\n    }\n  }, [showExportModal]);\n\n  // Get active file content\n  const activeExportFile =\n    selectedExportFile ||\n    (exportedFiles.length > 0 ? exportedFiles[0]?.path : null);\n  const activeExportContent =\n    exportedFiles.find((f) => f.path === activeExportFile)?.content || \"\";\n\n  // Get generated page code for the code tab\n  const generatedCode =\n    exportedFiles.find((f) => f.path === \"app/page.tsx\")?.content ||\n    \"// Generate a UI to see the code\";\n\n  const downloadAllFiles = useCallback(() => {\n    const allContent = exportedFiles\n      .map((f) => `// ========== ${f.path} ==========\\n${f.content}`)\n      .join(\"\\n\\n\");\n    const blob = new Blob([allContent], { type: \"text/plain\" });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = \"generated-app.txt\";\n    a.click();\n    URL.revokeObjectURL(url);\n    toast(\"Downloaded generated-app.txt\");\n  }, [exportedFiles]);\n\n  const copyFileContent = useCallback((content: string) => {\n    navigator.clipboard.writeText(content);\n    toast(\"Copied to clipboard\");\n  }, []);\n\n  const isTypingSimulation = mode === \"simulation\" && phase === \"typing\";\n  const isStreamingSimulation = mode === \"simulation\" && phase === \"streaming\";\n  const showLoadingDots = isStreamingSimulation || isStreaming;\n\n  const handleExampleClick = useCallback((prompt: string) => {\n    setMode(\"interactive\");\n    setPhase(\"complete\");\n    setUserPrompt(prompt);\n    setTimeout(() => {\n      const el = inputRef.current;\n      if (el) {\n        el.focus();\n        el.setSelectionRange(prompt.length, prompt.length);\n      }\n    }, 0);\n  }, []);\n\n  return (\n    <div\n      className={`w-full text-left ${fullscreen ? \"h-full flex flex-col\" : \"max-w-5xl mx-auto\"}`}\n    >\n      {/* Prompt input */}\n      <div className={fullscreen ? \"mb-4\" : \"mb-6\"}>\n        <div\n          className=\"border border-border rounded p-3 bg-background font-mono text-sm min-h-[44px] flex items-center justify-between cursor-text\"\n          onClick={() => {\n            if (mode === \"simulation\") {\n              setMode(\"interactive\");\n              setPhase(\"complete\");\n              setUserPrompt(\"\");\n              setTimeout(() => inputRef.current?.focus(), 0);\n            } else {\n              inputRef.current?.focus();\n            }\n          }}\n        >\n          {mode === \"simulation\" ? (\n            <div className=\"flex items-center flex-1\">\n              <span className=\"inline-flex items-center h-5\">\n                {typedPrompt}\n              </span>\n              {isTypingSimulation && (\n                <span className=\"inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse\" />\n              )}\n            </div>\n          ) : (\n            <form\n              className=\"flex items-center flex-1\"\n              onSubmit={(e) => {\n                e.preventDefault();\n                handleSubmit();\n              }}\n            >\n              <input\n                ref={inputRef}\n                type=\"text\"\n                value={userPrompt}\n                onChange={(e) => setUserPrompt(e.target.value)}\n                placeholder=\"Describe what you want to build...\"\n                className=\"flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50 text-base\"\n                disabled={isStreaming}\n                maxLength={140}\n              />\n            </form>\n          )}\n          {mode === \"simulation\" || isStreaming ? (\n            <button\n              onClick={(e) => {\n                e.stopPropagation();\n                stopGeneration();\n              }}\n              className=\"ml-2 w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n              aria-label=\"Stop\"\n            >\n              <svg\n                width=\"16\"\n                height=\"16\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n                stroke=\"none\"\n              >\n                <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" />\n              </svg>\n            </button>\n          ) : (\n            <button\n              onClick={(e) => {\n                e.stopPropagation();\n                handleSubmit();\n              }}\n              disabled={!userPrompt.trim()}\n              className=\"ml-2 w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n              aria-label=\"Submit\"\n            >\n              <svg\n                width=\"16\"\n                height=\"16\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M12 5v14\" />\n                <path d=\"M19 12l-7 7-7-7\" />\n              </svg>\n            </button>\n          )}\n        </div>\n        {fullscreen ? (\n          <div className=\"mt-3 flex flex-wrap gap-2 justify-center\">\n            {EXAMPLE_PROMPTS.map((prompt) => (\n              <button\n                key={prompt}\n                onClick={() => handleExampleClick(prompt)}\n                className=\"text-xs px-3 py-1.5 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors\"\n              >\n                {prompt}\n              </button>\n            ))}\n          </div>\n        ) : (\n          <div className=\"mt-2 flex flex-wrap gap-1.5 justify-center\">\n            {EXAMPLE_PROMPTS.slice(0, 2).map((prompt) => (\n              <button\n                key={prompt}\n                onClick={() => handleExampleClick(prompt)}\n                className=\"text-xs px-2 py-1 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors\"\n              >\n                {prompt}\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n\n      <div\n        className={`grid lg:grid-cols-2 gap-4 ${fullscreen ? \"flex-1 min-h-0\" : \"\"}`}\n      >\n        {/* Tabbed code/stream/json panel */}\n        <div className={`min-w-0 ${fullscreen ? \"flex flex-col\" : \"\"}`}>\n          <div className=\"flex items-center gap-4 mb-2 h-6 shrink-0\">\n            {([\"json\", \"nested\", \"stream\", \"catalog\"] as const).map((tab) => (\n              <button\n                key={tab}\n                onClick={() => setActiveTab(tab)}\n                className={`text-xs font-mono transition-colors ${\n                  activeTab === tab\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                }`}\n              >\n                {tab}\n              </button>\n            ))}\n          </div>\n          <div\n            className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? \"flex-1 min-h-0\" : \"h-[28rem]\"}`}\n          >\n            {activeTab !== \"catalog\" && (\n              <div className=\"absolute top-2 right-2 z-10\">\n                <CopyButton\n                  text={\n                    activeTab === \"stream\"\n                      ? streamLines.join(\"\\n\")\n                      : activeTab === \"nested\"\n                        ? nestedCode\n                        : jsonCode\n                  }\n                  className=\"opacity-0 group-hover:opacity-100 text-muted-foreground\"\n                />\n              </div>\n            )}\n            <div\n              className={`overflow-auto ${activeTab === \"stream\" ? \"\" : \"hidden\"}`}\n            >\n              {streamLines.length > 0 ? (\n                <>\n                  <CodeBlock\n                    code={streamLines.join(\"\\n\")}\n                    lang=\"json\"\n                    fillHeight\n                    hideCopyButton\n                  />\n                  {showLoadingDots && (\n                    <div className=\"flex gap-1 p-3 pt-0\">\n                      <span className=\"w-1 h-1 bg-muted-foreground rounded-full animate-pulse\" />\n                      <span className=\"w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:75ms]\" />\n                      <span className=\"w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:150ms]\" />\n                    </div>\n                  )}\n                </>\n              ) : (\n                <div className=\"text-muted-foreground/50 p-3 h-full\">\n                  {showLoadingDots ? \"streaming...\" : \"waiting...\"}\n                </div>\n              )}\n            </div>\n            <div\n              className={`overflow-auto ${activeTab === \"json\" ? \"\" : \"hidden\"}`}\n            >\n              <CodeBlock\n                code={jsonCode}\n                lang=\"json\"\n                fillHeight\n                hideCopyButton\n              />\n            </div>\n            <div\n              className={`overflow-auto ${activeTab === \"nested\" ? \"\" : \"hidden\"}`}\n            >\n              <CodeBlock\n                code={nestedCode}\n                lang=\"json\"\n                fillHeight\n                hideCopyButton\n              />\n            </div>\n            <div\n              className={`overflow-auto ${activeTab === \"catalog\" ? \"\" : \"hidden\"}`}\n            >\n              <div className=\"h-full flex flex-col text-sm font-sans\">\n                <div className=\"flex items-center gap-3 px-3 h-9 border-b border-border\">\n                  {(\n                    [\n                      {\n                        key: \"components\",\n                        label: `components (${catalogData.components.length})`,\n                      },\n                      {\n                        key: \"actions\",\n                        label: `actions (${catalogData.actions.length})`,\n                      },\n                    ] as const\n                  ).map(({ key, label }) => (\n                    <button\n                      key={key}\n                      onClick={() => setCatalogSection(key)}\n                      className={`text-xs font-mono transition-colors ${\n                        catalogSection === key\n                          ? \"text-foreground\"\n                          : \"text-muted-foreground hover:text-foreground\"\n                      }`}\n                    >\n                      {label}\n                    </button>\n                  ))}\n                </div>\n                <div className=\"flex-1 overflow-auto p-3\">\n                  {catalogSection === \"components\" ? (\n                    <div className=\"space-y-3\">\n                      {catalogData.components.map((comp) => (\n                        <div\n                          key={comp.name}\n                          className=\"pb-3 border-b border-border last:border-b-0\"\n                        >\n                          <div className=\"flex items-baseline gap-2 mb-1\">\n                            <span className=\"font-mono font-medium text-foreground\">\n                              {comp.name}\n                            </span>\n                            {comp.slots.length > 0 && (\n                              <span className=\"text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground\">\n                                slots: {comp.slots.join(\", \")}\n                              </span>\n                            )}\n                          </div>\n                          {comp.description && (\n                            <p className=\"text-xs text-muted-foreground mb-2\">\n                              {comp.description}\n                            </p>\n                          )}\n                          {comp.props.length > 0 && (\n                            <div className=\"flex flex-wrap gap-1 mb-1\">\n                              {comp.props.map((p) => (\n                                <span\n                                  key={p.name}\n                                  className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                                >\n                                  {p.name}\n                                  <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                    : {p.type}\n                                  </span>\n                                </span>\n                              ))}\n                            </div>\n                          )}\n                          {comp.events.length > 0 && (\n                            <div className=\"flex flex-wrap gap-1 mt-1.5\">\n                              {comp.events.map((e) => (\n                                <span\n                                  key={e}\n                                  className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\"\n                                >\n                                  on.{e}\n                                </span>\n                              ))}\n                            </div>\n                          )}\n                        </div>\n                      ))}\n                    </div>\n                  ) : (\n                    <div className=\"space-y-3\">\n                      {catalogData.actions.map((action) => (\n                        <div\n                          key={action.name}\n                          className=\"pb-3 border-b border-border last:border-b-0\"\n                        >\n                          <span className=\"font-mono font-medium text-foreground\">\n                            {action.name}\n                          </span>\n                          {action.description && (\n                            <p className=\"text-xs text-muted-foreground mt-1 mb-2\">\n                              {action.description}\n                            </p>\n                          )}\n                          {action.params.length > 0 && (\n                            <div className=\"flex flex-wrap gap-1\">\n                              {action.params.map((p) => (\n                                <span\n                                  key={p.name}\n                                  className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                                >\n                                  {p.name}\n                                  <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                    : {p.type}\n                                  </span>\n                                </span>\n                              ))}\n                            </div>\n                          )}\n                        </div>\n                      ))}\n                    </div>\n                  )}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Rendered output using json-render */}\n        <div className={`min-w-0 ${fullscreen ? \"flex flex-col\" : \"\"}`}>\n          <div className=\"flex items-center justify-between mb-2 h-6 shrink-0\">\n            <div className=\"flex items-center gap-4\">\n              {(\n                [\n                  { key: \"dynamic\", label: \"live render\" },\n                  { key: \"static\", label: \"static code\" },\n                ] as const\n              ).map(({ key, label }) => (\n                <button\n                  key={key}\n                  onClick={() => setRenderView(key)}\n                  className={`text-xs font-mono transition-colors ${\n                    renderView === key\n                      ? \"text-foreground\"\n                      : \"text-muted-foreground hover:text-foreground\"\n                  }`}\n                >\n                  {label}\n                </button>\n              ))}\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <button\n                onClick={() => setShowExportModal(true)}\n                disabled={!currentTree?.root}\n                className=\"text-xs font-mono text-muted-foreground hover:text-foreground transition-colors disabled:opacity-30 disabled:cursor-not-allowed\"\n                title=\"Export as Next.js project\"\n              >\n                export\n              </button>\n              <button\n                onClick={() => setIsFullscreen(true)}\n                className=\"text-muted-foreground hover:text-foreground transition-colors\"\n                aria-label=\"Maximize\"\n              >\n                <svg\n                  width=\"14\"\n                  height=\"14\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"M8 3H5a2 2 0 0 0-2 2v3\" />\n                  <path d=\"M21 8V5a2 2 0 0 0-2-2h-3\" />\n                  <path d=\"M3 16v3a2 2 0 0 0 2 2h3\" />\n                  <path d=\"M16 21h3a2 2 0 0 0 2-2v-3\" />\n                </svg>\n              </button>\n            </div>\n          </div>\n          <div\n            className={`border border-border rounded bg-background grid relative group ${fullscreen ? \"flex-1 min-h-0\" : \"h-[28rem]\"}`}\n          >\n            {renderView === \"static\" && (\n              <div className=\"absolute top-2 right-2 z-10\">\n                <CopyButton\n                  text={generatedCode}\n                  className=\"opacity-0 group-hover:opacity-100 text-muted-foreground\"\n                />\n              </div>\n            )}\n            {renderView === \"dynamic\" ? (\n              <div className=\"overflow-auto\">\n                {currentTree && currentTree.root ? (\n                  <div className=\"animate-in fade-in duration-200 w-full min-h-full flex items-center justify-center p-3 py-4\">\n                    <PlaygroundRenderer\n                      spec={currentTree}\n                      loading={isStreaming || isStreamingSimulation}\n                    />\n                  </div>\n                ) : (\n                  <div className=\"h-full flex items-center justify-center text-muted-foreground/50 text-sm\">\n                    {isStreaming ? \"generating...\" : \"waiting...\"}\n                  </div>\n                )}\n              </div>\n            ) : (\n              <div className=\"overflow-auto h-full font-mono text-xs text-left\">\n                <CodeBlock\n                  code={generatedCode}\n                  lang=\"tsx\"\n                  fillHeight\n                  hideCopyButton\n                />\n              </div>\n            )}\n          </div>\n          <Toaster position=\"bottom-right\" />\n        </div>\n      </div>\n\n      {/* Fullscreen modal */}\n      {isFullscreen && (\n        <div className=\"fixed inset-0 z-50 bg-background flex flex-col\">\n          <div className=\"flex items-center justify-between px-6 h-14 border-b border-border\">\n            <div className=\"text-sm font-mono\">render</div>\n            <button\n              onClick={() => setIsFullscreen(false)}\n              className=\"text-muted-foreground hover:text-foreground transition-colors p-1\"\n              aria-label=\"Close\"\n            >\n              <svg\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M18 6L6 18\" />\n                <path d=\"M6 6l12 12\" />\n              </svg>\n            </button>\n          </div>\n          <div className=\"flex-1 overflow-auto p-6\">\n            {currentTree && currentTree.root ? (\n              <div className=\"w-full min-h-full flex items-center justify-center\">\n                <PlaygroundRenderer\n                  spec={currentTree}\n                  loading={isStreaming || isStreamingSimulation}\n                />\n              </div>\n            ) : (\n              <div className=\"h-full flex items-center justify-center text-muted-foreground/50 text-sm\">\n                {isStreaming ? \"generating...\" : \"waiting...\"}\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n\n      {/* Export Modal */}\n      {showExportModal && (\n        <div className=\"fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 sm:p-8\">\n          <div className=\"bg-background border border-border rounded-lg w-full max-w-5xl h-full max-h-[80vh] flex flex-col shadow-2xl\">\n            {/* Header */}\n            <div className=\"flex items-center justify-between px-4 sm:px-6 h-14 border-b border-border shrink-0\">\n              <div className=\"flex items-center gap-2 sm:gap-3\">\n                {/* Mobile file tree toggle */}\n                <button\n                  onClick={() => setShowMobileFileTree(!showMobileFileTree)}\n                  className=\"sm:hidden text-muted-foreground hover:text-foreground transition-colors p-1\"\n                  aria-label=\"Toggle file tree\"\n                >\n                  <svg\n                    width=\"18\"\n                    height=\"18\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  >\n                    <path d=\"M3 6h18M3 12h18M3 18h18\" />\n                  </svg>\n                </button>\n                <span className=\"text-sm font-mono\">export static code</span>\n                <span className=\"text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded hidden sm:inline\">\n                  {exportedFiles.length} files\n                </span>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <button\n                  onClick={downloadAllFiles}\n                  className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs bg-foreground text-background rounded hover:bg-foreground/90 transition-colors\"\n                >\n                  <svg\n                    width=\"12\"\n                    height=\"12\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  >\n                    <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n                    <polyline points=\"7 10 12 15 17 10\" />\n                    <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\" />\n                  </svg>\n                  Download All\n                </button>\n                <button\n                  onClick={() => setShowExportModal(false)}\n                  className=\"text-muted-foreground hover:text-foreground transition-colors p-1\"\n                  aria-label=\"Close\"\n                >\n                  <svg\n                    width=\"20\"\n                    height=\"20\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  >\n                    <path d=\"M18 6L6 18\" />\n                    <path d=\"M6 6l12 12\" />\n                  </svg>\n                </button>\n              </div>\n            </div>\n\n            {/* Content */}\n            <div className=\"flex flex-1 min-h-0 relative\">\n              {/* File Tree - hidden on mobile, overlay when shown */}\n              <div\n                className={`\n                ${showMobileFileTree ? \"absolute inset-0 z-10 bg-background\" : \"hidden\"}\n                sm:relative sm:block sm:w-56 sm:bg-transparent\n                border-r border-border overflow-auto py-2\n              `}\n              >\n                {(() => {\n                  // Build tree structure from flat file list\n                  type TreeNode = {\n                    name: string;\n                    path: string;\n                    isFolder: boolean;\n                    children: TreeNode[];\n                    file?: { path: string; content: string };\n                  };\n\n                  const root: TreeNode = {\n                    name: \"\",\n                    path: \"\",\n                    isFolder: true,\n                    children: [],\n                  };\n\n                  exportedFiles.forEach((file) => {\n                    const parts = file.path.split(\"/\");\n                    let current = root;\n\n                    parts.forEach((part, idx) => {\n                      const isLast = idx === parts.length - 1;\n                      const path = parts.slice(0, idx + 1).join(\"/\");\n                      let child = current.children.find((c) => c.name === part);\n\n                      if (!child) {\n                        child = {\n                          name: part,\n                          path,\n                          isFolder: !isLast,\n                          children: [],\n                          file: isLast ? file : undefined,\n                        };\n                        current.children.push(child);\n                      }\n\n                      current = child;\n                    });\n                  });\n\n                  // Sort: folders first, then alphabetically\n                  const sortNodes = (nodes: TreeNode[]): TreeNode[] => {\n                    return nodes.sort((a, b) => {\n                      if (a.isFolder && !b.isFolder) return -1;\n                      if (!a.isFolder && b.isFolder) return 1;\n                      return a.name.localeCompare(b.name);\n                    });\n                  };\n\n                  const toggleFolder = (path: string) => {\n                    setCollapsedFolders((prev) => {\n                      const next = new Set(prev);\n                      if (next.has(path)) {\n                        next.delete(path);\n                      } else {\n                        next.add(path);\n                      }\n                      return next;\n                    });\n                  };\n\n                  const renderNode = (\n                    node: TreeNode,\n                    depth: number,\n                  ): React.ReactNode[] => {\n                    const result: React.ReactNode[] = [];\n                    const isExpanded = !collapsedFolders.has(node.path);\n\n                    if (node.isFolder && node.name) {\n                      result.push(\n                        <button\n                          key={`folder-${node.path}`}\n                          onClick={() => toggleFolder(node.path)}\n                          className=\"w-full text-left px-3 py-1 text-xs font-mono text-muted-foreground hover:text-foreground hover:bg-foreground/5 transition-colors\"\n                          style={{ paddingLeft: `${12 + depth * 12}px` }}\n                        >\n                          <span className=\"flex items-center gap-1.5\">\n                            <span\n                              className={`text-gray-400 transition-transform ${isExpanded ? \"rotate-90\" : \"\"}`}\n                            >\n                              <svg\n                                width=\"8\"\n                                height=\"8\"\n                                viewBox=\"0 0 24 24\"\n                                fill=\"currentColor\"\n                              >\n                                <path d=\"M8 5l10 7-10 7V5z\" />\n                              </svg>\n                            </span>\n                            <span className=\"text-gray-400\">\n                              <svg\n                                width=\"12\"\n                                height=\"12\"\n                                viewBox=\"0 0 24 24\"\n                                fill=\"currentColor\"\n                              >\n                                <path d=\"M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z\" />\n                              </svg>\n                            </span>\n                            {node.name}\n                          </span>\n                        </button>,\n                      );\n                    }\n\n                    if (node.file) {\n                      const isActive = node.file.path === activeExportFile;\n                      result.push(\n                        <button\n                          key={node.file.path}\n                          onClick={() => {\n                            setSelectedExportFile(node.file!.path);\n                            setShowMobileFileTree(false);\n                          }}\n                          className={`w-full text-left px-3 py-1 text-xs font-mono transition-colors ${\n                            isActive\n                              ? \"bg-foreground/10 text-foreground\"\n                              : \"text-muted-foreground hover:text-foreground hover:bg-foreground/5\"\n                          }`}\n                          style={{ paddingLeft: `${12 + depth * 12}px` }}\n                        >\n                          <span className=\"flex items-center gap-1.5\">\n                            {node.name.endsWith(\".tsx\") ||\n                            node.name.endsWith(\".ts\") ? (\n                              <span className=\"text-blue-400\">\n                                <svg\n                                  width=\"12\"\n                                  height=\"12\"\n                                  viewBox=\"0 0 24 24\"\n                                  fill=\"currentColor\"\n                                >\n                                  <path d=\"M3 3h18v18H3V3zm16.525 13.707c-.131-.821-.666-1.511-2.252-2.155-.552-.259-1.165-.438-1.349-.854-.068-.248-.083-.382-.039-.527.11-.373.458-.487.757-.381.193.07.37.258.482.52.51-.332.51-.332.86-.553-.132-.203-.203-.293-.297-.382-.335-.382-.78-.58-1.502-.558l-.375.047c-.361.09-.705.272-.923.531-.613.721-.437 1.976.245 2.494.674.476 1.661.59 1.791 1.052.12.543-.406.717-.919.65-.387-.071-.6-.273-.831-.641l-.871.529c.1.217.217.31.39.494.803.796 2.8.749 3.163-.476.013-.04.113-.33.071-.765zm-7.158-2.032c-.227.574-.446 1.148-.677 1.722-.204-.54-.42-1.102-.648-1.68l-.002-.02h-1.09v4.4h.798v-3.269l.796 2.011h.69l.793-2.012v3.27h.798v-4.4h-1.06l-.398 1.02v-.042zm-3.39-3.15v1.2h2.99v8.424h1.524v-8.424h2.99v-1.2H8.977z\" />\n                                </svg>\n                              </span>\n                            ) : node.name.endsWith(\".json\") ? (\n                              <span className=\"text-yellow-400\">\n                                <svg\n                                  width=\"12\"\n                                  height=\"12\"\n                                  viewBox=\"0 0 24 24\"\n                                  fill=\"none\"\n                                  stroke=\"currentColor\"\n                                  strokeWidth=\"2\"\n                                >\n                                  <path d=\"M4 4h16v16H4z\" />\n                                  <path d=\"M8 8h8M8 12h8M8 16h4\" />\n                                </svg>\n                              </span>\n                            ) : node.name.endsWith(\".css\") ? (\n                              <span className=\"text-pink-400\">\n                                <svg\n                                  width=\"12\"\n                                  height=\"12\"\n                                  viewBox=\"0 0 24 24\"\n                                  fill=\"currentColor\"\n                                >\n                                  <path d=\"M3 3h18v18H3V3zm15.751 10.875l-.634 7.125-6.125 2-6.125-2-.625-7.125h3.125l.312 3.625 3.313 1.125 3.312-1.125.375-3.625H6.125l-.313-3.125h12.376l-.312 3.125H9.125l.25 1.875h8.376v.125z\" />\n                                </svg>\n                              </span>\n                            ) : node.name.endsWith(\".md\") ? (\n                              <span className=\"text-gray-400\">\n                                <svg\n                                  width=\"12\"\n                                  height=\"12\"\n                                  viewBox=\"0 0 24 24\"\n                                  fill=\"currentColor\"\n                                >\n                                  <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM13 9V3.5L18.5 9H13z\" />\n                                </svg>\n                              </span>\n                            ) : (\n                              <span className=\"text-gray-400\">\n                                <svg\n                                  width=\"12\"\n                                  height=\"12\"\n                                  viewBox=\"0 0 24 24\"\n                                  fill=\"currentColor\"\n                                >\n                                  <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM13 9V3.5L18.5 9H13z\" />\n                                </svg>\n                              </span>\n                            )}\n                            {node.name}\n                          </span>\n                        </button>,\n                      );\n                    }\n\n                    // Only render children if not a folder or if folder is expanded (or root)\n                    if (!node.isFolder || !node.name || isExpanded) {\n                      sortNodes(node.children).forEach((child) => {\n                        result.push(\n                          ...renderNode(child, node.name ? depth + 1 : depth),\n                        );\n                      });\n                    }\n\n                    return result;\n                  };\n\n                  return renderNode(root, 0);\n                })()}\n              </div>\n\n              {/* Code Preview */}\n              <div className=\"flex-1 flex flex-col min-w-0\">\n                <div className=\"flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30\">\n                  <span className=\"text-xs font-mono text-muted-foreground\">\n                    {activeExportFile}\n                  </span>\n                  <button\n                    onClick={() => copyFileContent(activeExportContent)}\n                    className=\"text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1\"\n                  >\n                    <svg\n                      width=\"12\"\n                      height=\"12\"\n                      viewBox=\"0 0 24 24\"\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"2\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                    >\n                      <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n                      <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n                    </svg>\n                    Copy\n                  </button>\n                </div>\n                <div className=\"flex-1 overflow-auto\">\n                  <CodeBlock\n                    code={activeExportContent}\n                    lang={activeExportFile?.endsWith(\".json\") ? \"json\" : \"tsx\"}\n                    fillHeight\n                    hideCopyButton\n                  />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/docs-chat.tsx",
    "content": "\"use client\";\n\nimport {\n  useRef,\n  useEffect,\n  useState,\n  useCallback,\n  type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport { useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport } from \"ai\";\nimport { Streamdown } from \"streamdown\";\nimport Link from \"next/link\";\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/ui/sheet\";\n\nconst STORAGE_KEY = \"docs-chat-messages\";\nconst transport = new DefaultChatTransport({ api: \"/api/docs-chat\" });\n\nconst DESKTOP_DEFAULT_WIDTH = 400;\nconst DESKTOP_MIN_WIDTH = 300;\nconst DESKTOP_MAX_WIDTH = 700;\n\nfunction setCookie(name: string, value: string) {\n  document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`;\n}\n\nconst TOOL_LABELS: Record<\n  string,\n  { label: string; pastLabel: string; argKey?: string }\n> = {\n  readFile: { label: \"Reading\", pastLabel: \"Read\", argKey: \"path\" },\n  bash: { label: \"Running\", pastLabel: \"Ran\", argKey: \"command\" },\n};\n\nfunction isToolPart(part: { type: string }): part is {\n  type: string;\n  toolCallId: string;\n  toolName?: string;\n  state: string;\n  input?: Record<string, unknown>;\n  output?: unknown;\n  errorText?: string;\n} {\n  return part.type.startsWith(\"tool-\") || part.type === \"dynamic-tool\";\n}\n\nfunction getToolName(part: { type: string; toolName?: string }): string {\n  if (part.type === \"dynamic-tool\") return part.toolName ?? \"tool\";\n  return part.type.replace(/^tool-/, \"\");\n}\n\nfunction ToolCallDisplay({\n  part,\n}: {\n  part: {\n    type: string;\n    toolCallId: string;\n    toolName?: string;\n    state: string;\n    input?: Record<string, unknown>;\n    output?: unknown;\n    errorText?: string;\n  };\n}) {\n  const toolName = getToolName(part);\n  const config = TOOL_LABELS[toolName] ?? {\n    label: toolName,\n    pastLabel: toolName,\n  };\n  const isDone = part.state === \"output-available\";\n  const isError = part.state === \"output-error\";\n  const isRunning = !isDone && !isError;\n  const displayLabel = isRunning ? config.label : config.pastLabel;\n\n  const args = (part.input ?? {}) as Record<string, unknown>;\n  const argValue = config.argKey ? args[config.argKey] : undefined;\n  const argPreview =\n    argValue != null\n      ? String(argValue)\n          .replace(/\\/workspace\\//g, \"/\")\n          .replace(/\\.md$/, \"\")\n          .replace(/\\/index$/, \"\")\n      : \"\";\n\n  // Link to the docs page if it's a /docs/ path from readFile\n  const docsLink =\n    toolName === \"readFile\" &&\n    (argPreview === \"/docs\" || argPreview.startsWith(\"/docs/\"))\n      ? argPreview\n      : null;\n\n  const argEl = argPreview ? (\n    docsLink ? (\n      <Link href={docsLink} className=\"truncate underline underline-offset-2\">\n        {argPreview}\n      </Link>\n    ) : (\n      <span className=\"truncate\">{argPreview}</span>\n    )\n  ) : null;\n\n  return (\n    <div className=\"text-xs py-0.5 min-w-0\">\n      {isRunning ? (\n        <span className=\"inline-flex items-center gap-1 font-mono text-muted-foreground animate-tool-shimmer min-w-0 max-w-full\">\n          <span className=\"shrink-0\">{displayLabel}</span>\n          {argEl}\n        </span>\n      ) : (\n        <span className=\"inline-flex items-center gap-1 font-mono text-muted-foreground/60 min-w-0 max-w-full\">\n          <span className=\"shrink-0\">{displayLabel}</span>\n          {argEl}\n          {isError && <span className=\"text-destructive\">failed</span>}\n        </span>\n      )}\n    </div>\n  );\n}\n\nconst SUGGESTIONS = [\n  \"What is json-render?\",\n  \"How do I install it?\",\n  \"How does streaming work?\",\n  \"What components are available?\",\n  \"How do I create a custom schema?\",\n];\n\nexport function DocsChat({\n  defaultOpen = false,\n  defaultWidth = DESKTOP_DEFAULT_WIDTH,\n}: {\n  defaultOpen?: boolean;\n  defaultWidth?: number;\n}) {\n  const [open, setOpen] = useState(defaultOpen);\n  const [input, setInput] = useState(\"\");\n  const [isDesktop, setIsDesktop] = useState(false);\n  const [hasMounted, setHasMounted] = useState(false);\n  const [desktopWidth, setDesktopWidth] = useState(\n    Math.min(DESKTOP_MAX_WIDTH, Math.max(DESKTOP_MIN_WIDTH, defaultWidth)),\n  );\n  const messagesScrollRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const restoredRef = useRef(false);\n  const isDraggingRef = useRef(false);\n\n  const { messages, sendMessage, status, setMessages, error } = useChat({\n    transport,\n  });\n\n  const isLoading = status === \"streaming\" || status === \"submitted\";\n  const showMessages = messages.length > 0 || !!error || isLoading;\n\n  // Detect desktop vs mobile. Close sidebar on mobile if it was open from cookie.\n  useEffect(() => {\n    const mq = window.matchMedia(\"(min-width: 640px)\");\n    setIsDesktop(mq.matches);\n    setHasMounted(true);\n    // If on mobile but sidebar was open from cookie, close it\n    if (!mq.matches && defaultOpen) {\n      setOpen(false);\n    }\n    const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);\n    mq.addEventListener(\"change\", handler);\n    return () => mq.removeEventListener(\"change\", handler);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Persist open state to cookie (only after mount to avoid overwriting on mobile)\n  useEffect(() => {\n    if (hasMounted) {\n      setCookie(\"docs-chat-open\", String(open));\n    }\n  }, [open, hasMounted]);\n\n  // Push page content on desktop when pane is open.\n  // Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)\n  // instead of appearing right next to the sidebar's scrollbar.\n  useEffect(() => {\n    const body = document.body;\n    if (isDesktop && open) {\n      body.style.paddingRight = `${desktopWidth}px`;\n      if (!isDraggingRef.current) {\n        body.style.transition = \"padding-right 150ms ease\";\n      }\n    } else if (isDesktop) {\n      body.style.paddingRight = \"0px\";\n      body.style.transition = \"padding-right 150ms ease\";\n    }\n    return () => {\n      body.style.paddingRight = \"0px\";\n      body.style.transition = \"\";\n    };\n  }, [isDesktop, open, desktopWidth]);\n\n  // Resize handle drag\n  const handleResizePointerDown = useCallback(\n    (e: ReactPointerEvent<HTMLDivElement>) => {\n      e.preventDefault();\n      isDraggingRef.current = true;\n      document.documentElement.style.transition = \"none\";\n      const startX = e.clientX;\n      const startWidth = desktopWidth;\n\n      const onPointerMove = (ev: globalThis.PointerEvent) => {\n        const delta = startX - ev.clientX;\n        const newWidth = Math.min(\n          DESKTOP_MAX_WIDTH,\n          Math.max(DESKTOP_MIN_WIDTH, startWidth + delta),\n        );\n        setDesktopWidth(newWidth);\n      };\n\n      const onPointerUp = () => {\n        isDraggingRef.current = false;\n        document.documentElement.style.transition = \"\";\n        document.removeEventListener(\"pointermove\", onPointerMove);\n        document.removeEventListener(\"pointerup\", onPointerUp);\n      };\n\n      document.addEventListener(\"pointermove\", onPointerMove);\n      document.addEventListener(\"pointerup\", onPointerUp);\n    },\n    [desktopWidth],\n  );\n\n  // Persist width to cookie\n  useEffect(() => {\n    setCookie(\"docs-chat-width\", String(desktopWidth));\n  }, [desktopWidth]);\n\n  // Restore messages from sessionStorage on mount\n  useEffect(() => {\n    if (restoredRef.current) return;\n    restoredRef.current = true;\n    try {\n      const stored = sessionStorage.getItem(STORAGE_KEY);\n      if (stored) {\n        const parsed = JSON.parse(stored);\n        if (Array.isArray(parsed) && parsed.length > 0) {\n          setMessages(parsed);\n        }\n      }\n    } catch {\n      // ignore parse errors\n    }\n  }, [setMessages]);\n\n  // Save completed messages to sessionStorage\n  useEffect(() => {\n    if (!restoredRef.current) return;\n    if (isLoading) return;\n    if (messages.length === 0) {\n      sessionStorage.removeItem(STORAGE_KEY);\n      return;\n    }\n    try {\n      sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));\n    } catch {\n      // ignore quota errors\n    }\n  }, [messages, isLoading]);\n\n  // Cmd+I to open sidebar and focus prompt, Escape to close\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"i\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault();\n        setOpen((prev) => {\n          if (!prev) {\n            setTimeout(() => inputRef.current?.focus(), 200);\n          }\n          return !prev;\n        });\n      }\n      if (e.key === \"Escape\" && open && isDesktop) {\n        setOpen(false);\n      }\n    };\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [open, isDesktop]);\n\n  // Auto-focus input when opened\n  useEffect(() => {\n    if (open) {\n      const timer = setTimeout(() => inputRef.current?.focus(), 200);\n      return () => clearTimeout(timer);\n    }\n  }, [open]);\n\n  // Auto-open when error occurs\n  useEffect(() => {\n    if (error) setOpen(true);\n  }, [error]);\n\n  // Scroll to bottom when messages change or error occurs\n  useEffect(() => {\n    const el = messagesScrollRef.current;\n    if (!el) return;\n    requestAnimationFrame(() => {\n      el.scrollTop = el.scrollHeight;\n    });\n  }, [messages, error]);\n\n  const handleSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n      if (!input.trim() || isLoading) return;\n      sendMessage({ text: input });\n      setInput(\"\");\n    },\n    [input, isLoading, sendMessage],\n  );\n\n  const handleClear = useCallback(() => {\n    setMessages([]);\n    sessionStorage.removeItem(STORAGE_KEY);\n  }, [setMessages]);\n\n  const hasVisibleContent = (\n    parts: (typeof messages)[number][\"parts\"],\n  ): boolean => {\n    return parts.some(\n      (p) => (p.type === \"text\" && p.text.length > 0) || isToolPart(p),\n    );\n  };\n\n  // Shared chat panel content used by both desktop and mobile\n  const chatPanel = (\n    <>\n      {/* Header */}\n      <div className=\"flex items-center justify-between px-4 py-3 border-b shrink-0\">\n        <span className=\"text-sm font-medium\">json-render Docs</span>\n        <div className=\"flex items-center gap-3\">\n          {showMessages && (\n            <button\n              onClick={handleClear}\n              className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n              aria-label=\"Clear conversation\"\n            >\n              Clear\n            </button>\n          )}\n          <button\n            onClick={() => setOpen(false)}\n            className=\"text-muted-foreground hover:text-foreground transition-colors\"\n            aria-label=\"Close panel\"\n          >\n            <svg\n              width=\"14\"\n              height=\"14\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            >\n              <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n              <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n\n      {/* Content: suggestions or messages */}\n      {showMessages ? (\n        <div\n          ref={messagesScrollRef}\n          className=\"flex-1 min-h-0 p-4 space-y-4 overflow-y-auto\"\n        >\n          {messages.map((message) => {\n            if (!hasVisibleContent(message.parts)) return null;\n            return (\n              <div key={message.id}>\n                {message.role === \"user\" ? (\n                  <div className=\"text-sm text-muted-foreground whitespace-pre-wrap leading-relaxed\">\n                    {message.parts\n                      .filter(\n                        (p): p is Extract<typeof p, { type: \"text\" }> =>\n                          p.type === \"text\",\n                      )\n                      .map((p) => p.text)\n                      .join(\"\")}\n                  </div>\n                ) : (\n                  <div className=\"space-y-2\">\n                    {message.parts.map((part, i) => {\n                      if (part.type === \"text\" && part.text) {\n                        return (\n                          <div\n                            key={i}\n                            className=\"docs-chat-content text-sm text-foreground/90 leading-relaxed prose prose-sm dark:prose-invert max-w-none\"\n                          >\n                            <Streamdown>{part.text}</Streamdown>\n                          </div>\n                        );\n                      }\n                      if (isToolPart(part)) {\n                        return (\n                          <ToolCallDisplay key={part.toolCallId} part={part} />\n                        );\n                      }\n                      return null;\n                    })}\n                  </div>\n                )}\n              </div>\n            );\n          })}\n          {error && (\n            <div className=\"text-sm text-destructive/80 bg-destructive/10 rounded-md px-3 py-2\">\n              {(() => {\n                try {\n                  const parsed = JSON.parse(error.message);\n                  return parsed.message || parsed.error || error.message;\n                } catch {\n                  return (\n                    error.message || \"Something went wrong. Please try again.\"\n                  );\n                }\n              })()}\n            </div>\n          )}\n        </div>\n      ) : (\n        <div className=\"flex-1 min-h-0 flex flex-col\">\n          <div className=\"flex flex-wrap gap-2 p-4\">\n            {SUGGESTIONS.map((s) => (\n              <button\n                key={s}\n                type=\"button\"\n                onClick={() => {\n                  sendMessage({ text: s });\n                }}\n                className=\"text-xs px-3 py-1.5 rounded-full border bg-secondary font-medium text-muted-foreground hover:text-foreground transition-colors\"\n              >\n                {s}\n              </button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {/* Input bar */}\n      <form\n        onSubmit={handleSubmit}\n        className=\"flex items-end gap-2 px-4 py-3 border-t shrink-0\"\n      >\n        <textarea\n          ref={inputRef}\n          value={input}\n          onChange={(e) => {\n            setInput(e.target.value);\n            e.target.style.height = \"auto\";\n            e.target.style.height = `${e.target.scrollHeight}px`;\n          }}\n          rows={1}\n          enterKeyHint=\"send\"\n          placeholder=\"Ask a question...\"\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\" && !e.shiftKey) {\n              e.preventDefault();\n              handleSubmit(e);\n            }\n          }}\n          className=\"flex-1 bg-transparent text-base sm:text-sm text-foreground outline-none disabled:opacity-50 resize-none max-h-32 leading-relaxed placeholder:text-muted-foreground\"\n        />\n        <button\n          type=\"submit\"\n          disabled={isLoading || !input.trim()}\n          className=\"bg-primary text-primary-foreground rounded-full p-1.5 hover:bg-primary/90 transition-colors disabled:opacity-30 shrink-0\"\n          aria-label=\"Send message\"\n        >\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"5\" />\n            <polyline points=\"5 12 12 5 19 12\" />\n          </svg>\n        </button>\n      </form>\n    </>\n  );\n\n  return (\n    <>\n      {/* Ask AI trigger button */}\n      {!open && (\n        <button\n          onClick={() => setOpen(true)}\n          className=\"fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium\"\n          aria-label=\"Ask AI\"\n        >\n          Ask AI\n          <kbd className=\"hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono\">\n            <span>&#8984;</span>I\n          </kbd>\n        </button>\n      )}\n\n      {/* Desktop: resizable side pane — always rendered, hidden on mobile via CSS */}\n      <aside\n        className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l bg-background transition-transform duration-150 ease-in-out ${open ? \"translate-x-0\" : \"translate-x-full\"}`}\n        style={{ width: desktopWidth }}\n        aria-hidden={!open}\n      >\n        {/* Resize handle */}\n        <div\n          onPointerDown={handleResizePointerDown}\n          className=\"absolute top-0 bottom-0 left-0 w-1.5 cursor-col-resize hover:bg-ring/30 active:bg-ring/50 transition-colors z-10\"\n        />\n        <div className=\"flex flex-col flex-1 min-w-0\">{chatPanel}</div>\n      </aside>\n\n      {/* Mobile: Sheet overlay/drawer — only after mount to avoid flash on desktop */}\n      {hasMounted && !isDesktop && (\n        <Sheet open={open} onOpenChange={setOpen}>\n          <SheetContent\n            side=\"right\"\n            overlayClassName=\"!bg-background\"\n            className=\"!inset-0 !w-full !h-full !max-w-none p-0 flex flex-col\"\n            style={{ backgroundColor: \"var(--background)\", opacity: 1 }}\n          >\n            <SheetTitle className=\"sr-only\">AI Chat</SheetTitle>\n            {chatPanel}\n          </SheetContent>\n        </Sheet>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/docs-mobile-nav.tsx",
    "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { List } from \"lucide-react\";\nimport {\n  Sheet,\n  SheetTrigger,\n  SheetContent,\n  SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { docsNavigation, allDocsPages } from \"@/lib/docs-navigation\";\n\nexport function DocsMobileNav() {\n  const [open, setOpen] = useState(false);\n  const pathname = usePathname();\n\n  const currentPage = useMemo(() => {\n    const page = allDocsPages.find((page) => page.href === pathname);\n    return page ?? allDocsPages[0];\n  }, [pathname]);\n\n  return (\n    <Sheet open={open} onOpenChange={setOpen}>\n      <SheetTrigger className=\"lg:hidden sticky top-[calc(3.5rem+1px)] z-40 w-full px-6 py-3 bg-background/80 backdrop-blur-sm border-b border-border flex items-center justify-between focus:outline-none\">\n        <div className=\"text-sm font-medium\">{currentPage?.title}</div>\n        <div className=\"w-8 h-8 flex items-center justify-center\">\n          <List className=\"h-4 w-4 text-muted-foreground\" />\n        </div>\n      </SheetTrigger>\n      <SheetContent className=\"overflow-y-auto p-6\">\n        <SheetTitle className=\"mb-6\">Table of Contents</SheetTitle>\n        <nav className=\"space-y-6\">\n          {docsNavigation.map((section) => (\n            <div key={section.title}>\n              <h4 className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2\">\n                {section.title}\n              </h4>\n              <ul className=\"space-y-1\">\n                {section.items.map((item) => {\n                  const isExternal = item.external;\n                  return (\n                    <li key={item.href}>\n                      <Link\n                        href={item.href}\n                        onClick={() => setOpen(false)}\n                        {...(isExternal && {\n                          target: \"_blank\",\n                          rel: \"noopener noreferrer\",\n                        })}\n                        className={`text-sm block py-2 transition-colors ${\n                          pathname === item.href\n                            ? \"text-primary font-medium\"\n                            : \"text-muted-foreground hover:text-foreground\"\n                        } ${isExternal ? \"inline-flex items-center gap-1\" : \"\"}`}\n                      >\n                        {item.title}\n                        {isExternal && (\n                          <svg\n                            className=\"w-3 h-3\"\n                            fill=\"none\"\n                            stroke=\"currentColor\"\n                            viewBox=\"0 0 24 24\"\n                          >\n                            <path\n                              strokeLinecap=\"round\"\n                              strokeLinejoin=\"round\"\n                              strokeWidth={2}\n                              d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n                            />\n                          </svg>\n                        )}\n                      </Link>\n                    </li>\n                  );\n                })}\n              </ul>\n            </div>\n          ))}\n        </nav>\n      </SheetContent>\n    </Sheet>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/docs-sidebar.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/utils\";\nimport { docsNavigation } from \"@/lib/docs-navigation\";\n\nexport function DocsSidebar() {\n  const pathname = usePathname();\n\n  return (\n    <nav className=\"space-y-6 pb-8\">\n      {docsNavigation.map((section) => (\n        <div key={section.title}>\n          <h4 className=\"text-xs font-normal text-muted-foreground/50 uppercase tracking-wider mb-2\">\n            {section.title}\n          </h4>\n          <ul className=\"space-y-1\">\n            {section.items.map((item) => {\n              const isActive = pathname === item.href;\n              const isExternal = item.external;\n              return (\n                <li key={item.href}>\n                  <Link\n                    href={item.href}\n                    {...(isExternal && {\n                      target: \"_blank\",\n                      rel: \"noopener noreferrer\",\n                    })}\n                    className={cn(\n                      \"text-sm transition-colors block py-1\",\n                      isActive\n                        ? \"text-primary font-medium\"\n                        : \"text-muted-foreground hover:text-foreground\",\n                      isExternal && \"inline-flex items-center gap-1\",\n                    )}\n                  >\n                    {item.title}\n                    {isExternal && (\n                      <svg\n                        className=\"w-3 h-3\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        viewBox=\"0 0 24 24\"\n                      >\n                        <path\n                          strokeLinecap=\"round\"\n                          strokeLinejoin=\"round\"\n                          strokeWidth={2}\n                          d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n                        />\n                      </svg>\n                    )}\n                  </Link>\n                </li>\n              );\n            })}\n          </ul>\n        </div>\n      ))}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/expandable-code.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, useEffect } from \"react\";\n\ninterface ExpandableCodeProps {\n  children: React.ReactNode;\n  maxHeight?: number;\n}\n\nexport function ExpandableCode({\n  children,\n  maxHeight = 300,\n}: ExpandableCodeProps) {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [needsExpansion, setNeedsExpansion] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (contentRef.current) {\n      setNeedsExpansion(contentRef.current.scrollHeight > maxHeight);\n    }\n  }, [maxHeight]);\n\n  return (\n    <div className=\"relative\">\n      <div\n        ref={contentRef}\n        className=\"overflow-hidden transition-[max-height] duration-300\"\n        style={{\n          maxHeight: isExpanded || !needsExpansion ? \"none\" : maxHeight,\n        }}\n      >\n        {children}\n      </div>\n      {needsExpansion && !isExpanded && (\n        <>\n          <div className=\"absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-neutral-100 dark:from-[#0a0a0a] to-transparent pointer-events-none\" />\n          <button\n            onClick={() => setIsExpanded(true)}\n            className=\"absolute bottom-3 left-1/2 -translate-x-1/2 px-3 py-1.5 text-xs font-medium text-muted-foreground bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700 rounded-md transition-colors\"\n          >\n            Show all\n          </button>\n        </>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/generation-modes-diagram.tsx",
    "content": "\"use client\";\n\nfunction ScheduleItem({\n  time,\n  label,\n  color,\n}: {\n  time: string;\n  label: string;\n  color: string;\n}) {\n  return (\n    <div className=\"flex items-center gap-2 py-1.5 border-t border-border/50 first:border-t-0\">\n      <span className=\"text-[10px] text-muted-foreground/60 w-12 shrink-0 tabular-nums\">\n        {time}\n      </span>\n      <span\n        className=\"w-1.5 h-1.5 rounded-full shrink-0\"\n        style={{ background: color }}\n      />\n      <span className=\"text-[11px] text-muted-foreground\">{label}</span>\n    </div>\n  );\n}\n\nfunction InlineModeDiagram() {\n  return (\n    <div className=\"flex flex-col h-full\">\n      <div className=\"text-sm font-medium text-foreground text-center mb-3\">\n        Inline Mode\n      </div>\n      <div className=\"flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-col\">\n        <div className=\"flex-1 p-4 space-y-3 overflow-hidden\">\n          {/* User message */}\n          <div className=\"flex justify-end\">\n            <div className=\"bg-muted-foreground/20 rounded-2xl rounded-br px-3.5 py-2 max-w-[85%]\">\n              <span className=\"text-[11px] text-foreground/80\">\n                I have back-to-back meetings today\n              </span>\n            </div>\n          </div>\n\n          {/* AI response */}\n          <div className=\"space-y-2\">\n            <p className=\"text-[11px] text-muted-foreground/70\">\n              Packed day! Here&apos;s what you&apos;ve got:\n            </p>\n            <div className=\"bg-muted/40 border border-border rounded-xl p-3 w-[92%]\">\n              <div className=\"text-[11px] font-semibold text-foreground/80 mb-2\">\n                Today&apos;s Schedule\n              </div>\n              <ScheduleItem\n                time=\"10:00 AM\"\n                label=\"Design Review\"\n                color=\"#7aa2f7\"\n              />\n              <ScheduleItem\n                time=\"1:00 PM\"\n                label=\"Sprint Planning\"\n                color=\"#bb9af7\"\n              />\n              <ScheduleItem\n                time=\"3:30 PM\"\n                label=\"Team Standup\"\n                color=\"#9ece6a\"\n              />\n              <ScheduleItem\n                time=\"4:30 PM\"\n                label=\"Eng All-Hands\"\n                color=\"#e0af68\"\n              />\n            </div>\n          </div>\n        </div>\n\n        {/* Prompt input */}\n        <div className=\"p-2.5\">\n          <div className=\"bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5\">\n            <span className=\"text-[10px] text-muted-foreground/40 flex-1\">\n              Message...\n            </span>\n            <div className=\"w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center\">\n              <svg\n                className=\"w-2.5 h-2.5 text-muted-foreground/50\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n              >\n                <path\n                  d=\"M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z\"\n                  transform=\"rotate(-90 12 12)\"\n                />\n              </svg>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"mt-3 flex flex-col items-center gap-2\">\n        <div className=\"text-xs font-medium text-muted-foreground\">\n          AI decides when UI beats text\n        </div>\n        <div className=\"flex items-center gap-1.5 text-[10px] text-muted-foreground/50\">\n          <span>AI chatbots</span>\n          <span className=\"text-muted-foreground/30\">/</span>\n          <span>Copilots</span>\n          <span className=\"text-muted-foreground/30\">/</span>\n          <span>Assistants</span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LandingPage() {\n  return (\n    <div className=\"w-full h-full flex flex-col bg-muted/20\">\n      {/* Nav */}\n      <div className=\"flex items-center justify-between px-3 py-2 border-b border-border/50\">\n        <svg\n          width=\"12\"\n          height=\"12\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          className=\"shrink-0\"\n        >\n          <rect\n            x=\"2\"\n            y=\"14\"\n            width=\"5\"\n            height=\"8\"\n            rx=\"1\"\n            className=\"fill-muted-foreground/60\"\n          />\n          <rect\n            x=\"9.5\"\n            y=\"8\"\n            width=\"5\"\n            height=\"14\"\n            rx=\"1\"\n            className=\"fill-muted-foreground/60\"\n          />\n          <rect\n            x=\"17\"\n            y=\"2\"\n            width=\"5\"\n            height=\"20\"\n            rx=\"1\"\n            className=\"fill-muted-foreground/60\"\n          />\n        </svg>\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-[9px] text-muted-foreground/50\">Pricing</span>\n          <span className=\"text-[9px] text-muted-foreground/50\">Blog</span>\n          <span className=\"text-[8px] font-semibold bg-foreground text-background rounded-md px-1.5 py-0.5 whitespace-nowrap\">\n            Get Started\n          </span>\n        </div>\n      </div>\n\n      {/* Hero */}\n      <div className=\"flex-1 flex flex-col items-center justify-center gap-2.5 text-center px-4\">\n        <span className=\"text-[8px] font-medium text-green-400 bg-green-400/10 border border-green-400/15 rounded-full px-2 py-0.5\">\n          Trusted by 2,000+ teams\n        </span>\n        <div className=\"text-sm font-bold text-foreground leading-tight\">\n          Analytics that\n          <br />\n          move the needle\n        </div>\n        <div className=\"text-[10px] text-muted-foreground/50 leading-relaxed\">\n          Ship what matters. No setup required.\n        </div>\n        <div className=\"flex gap-1.5 mt-1\">\n          <button className=\"bg-foreground text-background text-[9px] font-semibold rounded-lg px-3 py-1.5 whitespace-nowrap\">\n            Start Free\n          </button>\n          <button className=\"border border-border text-muted-foreground text-[9px] font-medium rounded-lg px-3 py-1.5 whitespace-nowrap\">\n            Book Demo\n          </button>\n        </div>\n      </div>\n\n      {/* Footer */}\n      <div className=\"flex justify-center gap-3 py-2 border-t border-border/50\">\n        <span className=\"text-[8px] text-muted-foreground/30\">Privacy</span>\n        <span className=\"text-[8px] text-muted-foreground/30\">Terms</span>\n        <span className=\"text-[8px] text-muted-foreground/30\">Status</span>\n      </div>\n    </div>\n  );\n}\n\nfunction StandaloneModeDiagram() {\n  return (\n    <div className=\"flex flex-col h-full\">\n      <div className=\"text-sm font-medium text-foreground text-center mb-3\">\n        Standalone Mode\n      </div>\n      <div className=\"flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-row\">\n        {/* Left panel - conversation */}\n        <div className=\"w-[38%] border-r border-border/50 flex flex-col\">\n          <div className=\"flex-1 p-3 space-y-2.5\">\n            {/* User message */}\n            <div className=\"flex justify-end\">\n              <div className=\"bg-muted-foreground/20 rounded-2xl rounded-br px-2.5 py-1.5\">\n                <span className=\"text-[10px] text-foreground/80 block leading-relaxed\">\n                  A landing page for my analytics startup\n                </span>\n              </div>\n            </div>\n            {/* AI text */}\n            <p className=\"text-[10px] text-muted-foreground/60 leading-relaxed\">\n              Here&apos;s a clean landing page with a hero, nav, and CTAs.\n            </p>\n          </div>\n\n          {/* Prompt input */}\n          <div className=\"p-2.5\">\n            <div className=\"bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5\">\n              <span className=\"text-[10px] text-muted-foreground/40 flex-1\">\n                Message...\n              </span>\n              <div className=\"w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center\">\n                <svg\n                  className=\"w-2.5 h-2.5 text-muted-foreground/50\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"currentColor\"\n                >\n                  <path\n                    d=\"M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z\"\n                    transform=\"rotate(-90 12 12)\"\n                  />\n                </svg>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Right panel - landing page */}\n        <div className=\"flex-1\">\n          <LandingPage />\n        </div>\n      </div>\n      <div className=\"mt-3 flex flex-col items-center gap-2\">\n        <div className=\"text-xs font-medium text-muted-foreground\">\n          Prompt in, UI out\n        </div>\n        <div className=\"flex items-center gap-1.5 text-[10px] text-muted-foreground/50\">\n          <span>Website builders</span>\n          <span className=\"text-muted-foreground/30\">/</span>\n          <span>Text-to-widget</span>\n          <span className=\"text-muted-foreground/30\">/</span>\n          <span>Dashboards</span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function GenerationModesDiagram() {\n  return (\n    <div className=\"not-prose my-8\">\n      <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-6\">\n        <div className=\"h-[420px]\">\n          <InlineModeDiagram />\n        </div>\n        <div className=\"h-[420px]\">\n          <StandaloneModeDiagram />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/header.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { ThemeToggle } from \"./theme-toggle\";\nimport { Search } from \"./search\";\nimport {\n  Sheet,\n  SheetTrigger,\n  SheetContent,\n  SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { cn } from \"@/lib/utils\";\n\nconst navLinks = [\n  { href: \"/playground\", label: \"Playground\" },\n  { href: \"/examples\", label: \"Examples\" },\n  { href: \"/docs\", label: \"Docs\" },\n];\n\nfunction GitHubLink({ className }: { className?: string }) {\n  return (\n    <a\n      href=\"https://github.com/vercel-labs/json-render\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={cn(\n        \"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors\",\n        className,\n      )}\n    >\n      <svg\n        viewBox=\"0 0 16 16\"\n        className=\"h-4 w-4\"\n        fill=\"currentColor\"\n        aria-hidden=\"true\"\n      >\n        <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n      </svg>\n      <span>12k</span>\n    </a>\n  );\n}\n\nexport function Header() {\n  const pathname = usePathname();\n  const [mobileOpen, setMobileOpen] = useState(false);\n\n  const isActive = (href: string) => {\n    if (href === \"/playground\") {\n      return pathname === \"/playground\";\n    }\n    if (href === \"/examples\") {\n      return pathname.startsWith(\"/examples\");\n    }\n    if (href === \"/docs\") {\n      return pathname.startsWith(\"/docs\");\n    }\n    return false;\n  };\n\n  return (\n    <header className=\"sticky top-0 z-50 bg-background\">\n      <div className=\"flex h-14 items-center justify-between px-4 gap-6\">\n        <div className=\"flex items-center gap-2\">\n          <Link href=\"https://vercel.com\" title=\"Made with love by Vercel\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"18\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"18\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M8 1L16 15H0L8 1Z\"\n                fill=\"currentColor\"\n              ></path>\n            </svg>\n          </Link>\n          <span className=\"text-(--ds-gray-500)\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"16\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"16\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z\"\n                fill=\"currentColor\"\n              ></path>\n            </svg>\n          </span>\n          <Link href=\"/\">\n            <span className=\"font-medium tracking-tight text-lg font-(family-name:--font-geist-pixel-square)\">\n              json-render\n            </span>\n          </Link>\n        </div>\n\n        {/* Desktop nav */}\n        <nav className=\"hidden sm:flex items-center gap-4\">\n          {navLinks.map((link) => (\n            <Link\n              key={link.href}\n              href={link.href}\n              className={cn(\n                \"text-sm transition-colors\",\n                isActive(link.href)\n                  ? \"text-primary\"\n                  : \"text-muted-foreground hover:text-foreground\",\n              )}\n            >\n              {link.label}\n            </Link>\n          ))}\n          <Search />\n          <GitHubLink />\n          <ThemeToggle />\n        </nav>\n\n        {/* Mobile nav */}\n        <div className=\"flex sm:hidden items-center gap-3\">\n          <Search />\n          <GitHubLink />\n          <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>\n            <SheetTrigger\n              className=\"flex items-center justify-center\"\n              aria-label=\"Open menu\"\n            >\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                className=\"text-muted-foreground\"\n              >\n                <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" />\n                <line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" />\n                <line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />\n              </svg>\n            </SheetTrigger>\n            <SheetContent side=\"right\" className=\"overflow-y-auto p-6\">\n              <SheetTitle className=\"mb-6\">Menu</SheetTitle>\n              <nav className=\"flex flex-col gap-1\">\n                {navLinks.map((link) => (\n                  <Link\n                    key={link.href}\n                    href={link.href}\n                    onClick={() => setMobileOpen(false)}\n                    className={cn(\n                      \"block py-2.5 text-sm transition-colors\",\n                      isActive(link.href)\n                        ? \"text-primary\"\n                        : \"text-muted-foreground hover:text-foreground\",\n                    )}\n                  >\n                    {link.label}\n                  </Link>\n                ))}\n                <div className=\"my-3 border-t border-border\" />\n                <div className=\"flex items-center justify-between py-2.5\">\n                  <span className=\"text-sm text-muted-foreground\">Theme</span>\n                  <ThemeToggle />\n                </div>\n              </nav>\n            </SheetContent>\n          </Sheet>\n        </div>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/package-install.tsx",
    "content": "import { codeToHtml } from \"shiki\";\nimport { CodeTabs } from \"./code-tabs\";\n\nconst vercelDarkTheme = {\n  name: \"vercel-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#666666\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#7928CA\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#888888\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n  ],\n};\n\nconst vercelLightTheme = {\n  name: \"vercel-light\",\n  type: \"light\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#171717\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#6e56cf\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n  ],\n};\n\ninterface PackageInstallProps {\n  packages: string;\n}\n\nconst packageManagers = [\n  { label: \"npm\", value: \"npm\", command: \"npm install\" },\n  { label: \"pnpm\", value: \"pnpm\", command: \"pnpm add\" },\n  { label: \"yarn\", value: \"yarn\", command: \"yarn add\" },\n  { label: \"bun\", value: \"bun\", command: \"bun add\" },\n];\n\nexport async function PackageInstall({ packages }: PackageInstallProps) {\n  const tabs = await Promise.all(\n    packageManagers.map(async (pm) => {\n      const code = `${pm.command} ${packages}`;\n      const html = await codeToHtml(code, {\n        lang: \"bash\",\n        themes: {\n          light: vercelLightTheme,\n          dark: vercelDarkTheme,\n        },\n        defaultColor: false,\n      });\n      return {\n        label: pm.label,\n        value: pm.value,\n        code,\n        html,\n      };\n    }),\n  );\n\n  return <CodeTabs tabs={tabs} />;\n}\n"
  },
  {
    "path": "apps/web/components/playground.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState, useCallback, useRef, useMemo } from \"react\";\nimport { flushSync } from \"react-dom\";\nimport type { Spec } from \"@json-render/core\";\nimport { collectUsedComponents, serializeProps } from \"@json-render/codegen\";\nimport { toast } from \"sonner\";\nimport { stringify as yamlStringify } from \"yaml\";\nimport type { EditMode } from \"@json-render/core\";\nimport {\n  usePlaygroundStream,\n  type StreamFormat,\n  type TokenUsage,\n} from \"@/lib/use-playground-stream\";\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n} from \"@/components/ui/resizable\";\nimport { CodeBlock } from \"./code-block\";\nimport { CopyButton } from \"./copy-button\";\nimport { Toaster } from \"./ui/sonner\";\nimport { Header } from \"./header\";\nimport { Sheet, SheetContent, SheetTitle } from \"./ui/sheet\";\nimport { JsonEditor } from \"@visual-json/react\";\nimport type { JsonValue } from \"@visual-json/react\";\nimport { PlaygroundRenderer } from \"@/lib/render/renderer\";\nimport { playgroundCatalog } from \"@/lib/render/catalog\";\nimport { buildCatalogDisplayData } from \"@/lib/render/catalog-display\";\n\ntype Tab = \"spec\" | \"nested\" | \"stream\" | \"catalog\" | \"visual\";\ntype RenderView = \"preview\" | \"code\";\ntype MobileView =\n  | \"spec\"\n  | \"nested\"\n  | \"stream\"\n  | \"catalog\"\n  | \"visual\"\n  | \"preview\"\n  | \"generated-code\";\n\ninterface Version {\n  id: string;\n  prompt: string;\n  tree: Spec | null;\n  status: \"generating\" | \"complete\" | \"error\";\n  usage: TokenUsage | null;\n  rawLines: string[];\n  format: StreamFormat;\n}\n\nfunction formatTokens(n: number): string {\n  if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\\.0$/, \"\")}k`;\n  return String(n);\n}\n\nfunction PlaygroundControls({\n  format,\n  setFormat,\n  editModes,\n  setEditModes,\n  showClear,\n  onClear,\n}: {\n  format: StreamFormat;\n  setFormat: (f: StreamFormat) => void;\n  editModes: EditMode[];\n  setEditModes: React.Dispatch<React.SetStateAction<EditMode[]>>;\n  showClear: boolean;\n  onClear: () => void;\n}) {\n  return (\n    <div className=\"flex items-center gap-2\">\n      <div className=\"flex items-center rounded border border-border text-[10px] font-mono overflow-hidden\">\n        {([\"jsonl\", \"yaml\"] as const).map((f) => (\n          <button\n            key={f}\n            onClick={() => setFormat(f)}\n            className={`px-1.5 py-0.5 transition-colors ${\n              format === f\n                ? \"bg-muted text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\"\n            }`}\n          >\n            {f}\n          </button>\n        ))}\n      </div>\n      <div className=\"flex items-center rounded border border-border text-[10px] font-mono overflow-hidden\">\n        {([\"patch\", \"merge\", \"diff\"] as const).map((m) => (\n          <button\n            key={m}\n            onClick={() => {\n              setEditModes((prev) =>\n                prev.includes(m)\n                  ? prev.length > 1\n                    ? prev.filter((x) => x !== m)\n                    : prev\n                  : [...prev, m],\n              );\n            }}\n            className={`px-1.5 py-0.5 transition-colors ${\n              editModes.includes(m)\n                ? \"bg-muted text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\"\n            }`}\n          >\n            {m}\n          </button>\n        ))}\n      </div>\n      {showClear && (\n        <button\n          onClick={onClear}\n          className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n        >\n          Clear\n        </button>\n      )}\n    </div>\n  );\n}\n\n/**\n * Convert a flat Spec into a nested tree structure that is easier for humans\n * to read. Children keys are resolved recursively into inline objects.\n */\nfunction specToNested(spec: Spec): Record<string, unknown> {\n  function resolve(key: string): Record<string, unknown> {\n    const el = spec.elements[key];\n    if (!el) return { _key: key, _missing: true };\n\n    const node: Record<string, unknown> = { type: el.type };\n\n    if (el.props && Object.keys(el.props).length > 0) {\n      node.props = el.props;\n    }\n\n    if (el.visible !== undefined) {\n      node.visible = el.visible;\n    }\n\n    if (el.on && Object.keys(el.on).length > 0) {\n      node.on = el.on;\n    }\n\n    if (el.repeat) {\n      node.repeat = el.repeat;\n    }\n\n    if (el.children && el.children.length > 0) {\n      node.children = el.children.map(resolve);\n    }\n\n    return node;\n  }\n\n  const result: Record<string, unknown> = {};\n\n  if (spec.state && Object.keys(spec.state).length > 0) {\n    result.state = spec.state;\n  }\n\n  result.elements = resolve(spec.root);\n\n  return result;\n}\n\nconst EXAMPLE_PROMPTS = [\n  \"Create a login form\",\n  \"Build a pricing page\",\n  \"Design a user profile card\",\n  \"Make a contact form\",\n];\n\nexport function Playground() {\n  const [versions, setVersions] = useState<Version[]>([]);\n  const [selectedVersionId, setSelectedVersionId] = useState<string | null>(\n    null,\n  );\n  const [inputValue, setInputValue] = useState(\"\");\n  const [activeTab, setActiveTab] = useState<Tab>(\"spec\");\n  const [catalogSection, setCatalogSection] = useState<\n    \"components\" | \"actions\"\n  >(\"components\");\n  const [renderView, setRenderView] = useState<RenderView>(\"preview\");\n  const [mobileView, setMobileView] = useState<MobileView>(\"preview\");\n  const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);\n  const [format, setFormat] = useState<StreamFormat>(\"jsonl\");\n  const [editModes, setEditModes] = useState<EditMode[]>([\"patch\"]);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const mobileInputRef = useRef<HTMLTextAreaElement>(null);\n  const versionsEndRef = useRef<HTMLDivElement>(null);\n\n  // Track the currently generating version ID\n  const generatingVersionIdRef = useRef<string | null>(null);\n\n  // Track the current tree for use as previousSpec in next generation\n  const currentTreeRef = useRef<Spec | null>(null);\n\n  const {\n    spec: apiSpec,\n    isStreaming,\n    usage: streamUsage,\n    rawLines: streamRawLines,\n    send,\n    clear,\n  } = usePlaygroundStream({\n    api: \"/api/generate\",\n    format,\n    editModes,\n    onError: (err: Error) => {\n      console.error(\"Generation error:\", err);\n      toast.error(err.message || \"Generation failed. Please try again.\");\n      if (generatingVersionIdRef.current) {\n        const erroredVersionId = generatingVersionIdRef.current;\n        setVersions((prev) =>\n          prev.map((v) =>\n            v.id === erroredVersionId ? { ...v, status: \"error\" as const } : v,\n          ),\n        );\n        generatingVersionIdRef.current = null;\n      }\n    },\n  });\n\n  // Get the selected version\n  const selectedVersion = versions.find((v) => v.id === selectedVersionId);\n\n  // Determine which tree to display:\n  // - If streaming and selected version is the generating one, show apiSpec\n  // - Otherwise show the selected version's tree\n  const isSelectedVersionGenerating =\n    selectedVersionId === generatingVersionIdRef.current && isStreaming;\n  const hasValidApiTree =\n    apiSpec && apiSpec.root && Object.keys(apiSpec.elements).length > 0;\n\n  const currentTree =\n    isSelectedVersionGenerating && hasValidApiTree\n      ? apiSpec\n      : (selectedVersion?.tree ??\n        (isSelectedVersionGenerating ? apiSpec : null));\n\n  // Raw JSONL lines: live from stream during generation, or stored per version\n  const currentRawLines = isSelectedVersionGenerating\n    ? streamRawLines\n    : (selectedVersion?.rawLines ?? []);\n\n  // Keep the ref updated with the current tree for use in handleSubmit\n  if (\n    currentTree &&\n    currentTree.root &&\n    Object.keys(currentTree.elements).length > 0\n  ) {\n    currentTreeRef.current = currentTree;\n  }\n\n  // Scroll to bottom when versions change\n  useEffect(() => {\n    versionsEndRef.current?.scrollIntoView({ behavior: \"smooth\" });\n  }, [versions]);\n\n  // Update version when streaming completes\n  useEffect(() => {\n    if (\n      !isStreaming &&\n      apiSpec &&\n      apiSpec.root &&\n      generatingVersionIdRef.current\n    ) {\n      const completedVersionId = generatingVersionIdRef.current;\n      setVersions((prev) =>\n        prev.map((v) =>\n          v.id === completedVersionId\n            ? {\n                ...v,\n                tree: apiSpec,\n                status: \"complete\" as const,\n                usage: streamUsage,\n                rawLines: streamRawLines,\n              }\n            : v,\n        ),\n      );\n      generatingVersionIdRef.current = null;\n    }\n  }, [isStreaming, apiSpec, streamUsage, streamRawLines]);\n\n  const handleSubmit = useCallback(async () => {\n    if (!inputValue.trim() || isStreaming) return;\n\n    const newVersionId = Date.now().toString();\n    const newVersion: Version = {\n      id: newVersionId,\n      prompt: inputValue.trim(),\n      tree: null,\n      status: \"generating\",\n      usage: null,\n      rawLines: [],\n      format,\n    };\n\n    generatingVersionIdRef.current = newVersionId;\n    setVersions((prev) => [...prev, newVersion]);\n    setSelectedVersionId(newVersionId);\n    setInputValue(\"\");\n\n    // Pass the current tree as context so the API can iterate on it\n    await send(inputValue.trim(), { previousSpec: currentTreeRef.current });\n  }, [inputValue, isStreaming, send, format]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleSubmit();\n      }\n    },\n    [handleSubmit],\n  );\n\n  const handleVisualChange = useCallback(\n    (value: JsonValue) => {\n      if (!selectedVersionId || isStreaming) return;\n      setVersions((prev) =>\n        prev.map((v) =>\n          v.id === selectedVersionId\n            ? { ...v, tree: value as unknown as Spec }\n            : v,\n        ),\n      );\n    },\n    [selectedVersionId, isStreaming],\n  );\n\n  const specCode = useMemo(() => {\n    if (!currentTree)\n      return format === \"yaml\" ? \"# waiting...\" : \"// waiting...\";\n    if (format === \"yaml\") {\n      return yamlStringify(currentTree, { indent: 2 }).trimEnd();\n    }\n    return JSON.stringify(currentTree, null, 2);\n  }, [currentTree, format]);\n\n  const specLang = format === \"yaml\" ? \"yaml\" : \"json\";\n\n  const nestedCode = useMemo(() => {\n    if (!currentTree || !currentTree.root) return \"// waiting...\";\n    return JSON.stringify(specToNested(currentTree), null, 2);\n  }, [currentTree]);\n\n  const generatedCode = useMemo(() => {\n    if (!currentTree || !currentTree.root) {\n      return \"// Generate a UI to see the code\";\n    }\n\n    const tree = currentTree;\n    const components = collectUsedComponents(tree);\n\n    function generateJSX(key: string, indent: number): string {\n      const element = tree.elements[key];\n      if (!element) return \"\";\n\n      const spaces = \"  \".repeat(indent);\n      const componentName = element.type;\n\n      const propsObj: Record<string, unknown> = {};\n      for (const [k, v] of Object.entries(element.props ?? {})) {\n        if (v !== null && v !== undefined) {\n          propsObj[k] = v;\n        }\n      }\n\n      const propsStr = serializeProps(propsObj);\n      const hasChildren = element.children && element.children.length > 0;\n\n      if (!hasChildren) {\n        return propsStr\n          ? `${spaces}<${componentName} ${propsStr} />`\n          : `${spaces}<${componentName} />`;\n      }\n\n      const lines: string[] = [];\n      lines.push(\n        propsStr\n          ? `${spaces}<${componentName} ${propsStr}>`\n          : `${spaces}<${componentName}>`,\n      );\n\n      for (const childKey of element.children!) {\n        lines.push(generateJSX(childKey, indent + 1));\n      }\n\n      lines.push(`${spaces}</${componentName}>`);\n      return lines.join(\"\\n\");\n    }\n\n    const jsx = generateJSX(tree.root, 2);\n    const imports = Array.from(components).sort().join(\", \");\n\n    return `\"use client\";\n\nimport { ${imports} } from \"@/components/ui\";\n\nexport default function Page() {\n  return (\n    <div className=\"min-h-screen p-8 flex items-center justify-center\">\n${jsx}\n    </div>\n  );\n}`;\n  }, [currentTree]);\n\n  // Determine syntax lang for raw stream based on selected version's format\n  const streamLang = isSelectedVersionGenerating\n    ? format === \"yaml\"\n      ? \"yaml\"\n      : \"json\"\n    : selectedVersion?.format === \"yaml\"\n      ? \"yaml\"\n      : \"json\";\n\n  // Chat pane content\n  const chatPane = (\n    <div className=\"h-full flex flex-col border-t border-border\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center\">\n        <span className=\"text-xs font-mono text-muted-foreground\">\n          versions\n        </span>\n      </div>\n      <div\n        className={`flex-1 p-2 min-h-0 ${versions.length > 0 ? \"overflow-y-auto space-y-1\" : \"flex\"}`}\n      >\n        {versions.length === 0 ? (\n          <div className=\"flex-1 flex flex-col items-center justify-center text-center px-4\">\n            <p className=\"text-sm text-muted-foreground mb-4\">\n              Describe what you want to build, then iterate on it.\n            </p>\n            <div className=\"flex flex-wrap gap-2 justify-center\">\n              {EXAMPLE_PROMPTS.map((prompt) => (\n                <button\n                  key={prompt}\n                  onMouseDown={(e) => {\n                    e.preventDefault();\n                    flushSync(() => setInputValue(prompt));\n                    // chatPane is rendered in both desktop and mobile layouts,\n                    // so inputRef may point to the hidden instance. Find the\n                    // textarea in the same layout container as the clicked button.\n                    const container = (e.currentTarget as HTMLElement).closest(\n                      \".h-full.flex.flex-col\",\n                    );\n                    const el =\n                      container?.querySelector<HTMLTextAreaElement>(\n                        \"textarea\",\n                      ) ?? inputRef.current;\n                    if (el) {\n                      el.focus();\n                      el.setSelectionRange(prompt.length, prompt.length);\n                    }\n                  }}\n                  className=\"text-xs px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors\"\n                >\n                  {prompt}\n                </button>\n              ))}\n            </div>\n          </div>\n        ) : (\n          versions.map((version, index) => (\n            <button\n              key={version.id}\n              onClick={() => setSelectedVersionId(version.id)}\n              className={`w-full text-left px-3 py-2 rounded text-sm transition-colors ${\n                selectedVersionId === version.id\n                  ? \"bg-muted text-foreground\"\n                  : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\"\n              }`}\n            >\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-xs font-mono text-muted-foreground/70 shrink-0\">\n                  v{index + 1}\n                </span>\n                <span className=\"truncate flex-1\">{version.prompt}</span>\n                {version.status === \"generating\" && (\n                  <span className=\"text-xs text-muted-foreground shrink-0 animate-pulse\">\n                    ...\n                  </span>\n                )}\n                {version.status === \"error\" && (\n                  <span className=\"text-xs text-red-500 shrink-0\">failed</span>\n                )}\n              </div>\n              {version.usage && (\n                <div className=\"mt-1 ml-6\">\n                  <span className=\"text-[10px] font-mono text-muted-foreground/60\">\n                    {formatTokens(\n                      version.usage.promptTokens - version.usage.cachedTokens,\n                    )}{\" \"}\n                    in · {formatTokens(version.usage.completionTokens)} out\n                    {version.usage.cachedTokens > 0\n                      ? ` · ${formatTokens(version.usage.cachedTokens)} cached`\n                      : \"\"}\n                  </span>\n                </div>\n              )}\n            </button>\n          ))\n        )}\n        <div ref={versionsEndRef} />\n      </div>\n      <div\n        className=\"border-t border-border p-3 cursor-text\"\n        onMouseDown={(e) => {\n          // Focus textarea unless clicking a button or the textarea itself\n          const target = e.target as HTMLElement;\n          if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n            e.preventDefault();\n            inputRef.current?.focus();\n          }\n        }}\n      >\n        <textarea\n          ref={inputRef}\n          value={inputValue}\n          onChange={(e) => setInputValue(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder=\"Describe changes...\"\n          className=\"w-full bg-background text-base sm:text-sm resize-none outline-none placeholder:text-muted-foreground/50\"\n          rows={2}\n          autoFocus\n        />\n        <div className=\"flex justify-between items-center mt-2\">\n          <PlaygroundControls\n            format={format}\n            setFormat={setFormat}\n            editModes={editModes}\n            setEditModes={setEditModes}\n            showClear={versions.length > 0}\n            onClear={() => {\n              setVersions([]);\n              setSelectedVersionId(null);\n              clear();\n            }}\n          />\n          {isStreaming ? (\n            <button\n              onClick={() => clear()}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n              aria-label=\"Stop\"\n            >\n              <svg\n                width=\"14\"\n                height=\"14\"\n                viewBox=\"0 0 24 24\"\n                fill=\"currentColor\"\n                stroke=\"none\"\n              >\n                <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" />\n              </svg>\n            </button>\n          ) : (\n            <button\n              onClick={handleSubmit}\n              disabled={!inputValue.trim()}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n              aria-label=\"Send\"\n            >\n              <svg\n                width=\"14\"\n                height=\"14\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d=\"M5 12h14\" />\n                <path d=\"m12 5 7 7-7 7\" />\n              </svg>\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  // Catalog data for the catalog tab\n  const catalogData = useMemo(\n    () => buildCatalogDisplayData(playgroundCatalog.data),\n    [],\n  );\n\n  // Code pane content\n  const copyText =\n    activeTab === \"stream\"\n      ? currentRawLines.join(\"\\n\")\n      : activeTab === \"spec\"\n        ? specCode\n        : activeTab === \"nested\"\n          ? nestedCode\n          : activeTab === \"visual\"\n            ? specCode\n            : \"\";\n\n  const codePane = (\n    <div className=\"h-full flex flex-col border-t border-border\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        {([\"spec\", \"visual\", \"nested\", \"stream\", \"catalog\"] as const).map(\n          (tab) => (\n            <button\n              key={tab}\n              onClick={() => setActiveTab(tab)}\n              className={`text-xs font-mono transition-colors ${\n                activeTab === tab\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground hover:text-foreground\"\n              }`}\n            >\n              {tab === \"spec\" ? (format === \"yaml\" ? \"yaml\" : \"json\") : tab}\n            </button>\n          ),\n        )}\n        <div className=\"flex-1\" />\n        {activeTab !== \"catalog\" && activeTab !== \"visual\" && (\n          <CopyButton text={copyText} className=\"text-muted-foreground\" />\n        )}\n      </div>\n      <div className=\"flex-1 overflow-auto\">\n        {activeTab === \"visual\" ? (\n          currentTree ? (\n            <JsonEditor\n              value={currentTree as unknown as JsonValue}\n              onChange={handleVisualChange}\n              readOnly={isStreaming}\n              sidebarOpen={false}\n              height=\"100%\"\n              className=\"h-full\"\n              style={\n                {\n                  \"--vj-bg\": \"var(--background)\",\n                  \"--vj-bg-panel\": \"var(--background)\",\n                  \"--vj-bg-hover\": \"var(--muted)\",\n                  \"--vj-bg-selected\": \"var(--primary)\",\n                  \"--vj-bg-selected-muted\": \"var(--muted)\",\n                  \"--vj-text\": \"var(--foreground)\",\n                  \"--vj-text-selected\": \"var(--primary-foreground)\",\n                  \"--vj-text-muted\": \"var(--muted-foreground)\",\n                  \"--vj-text-dim\": \"var(--muted-foreground)\",\n                  \"--vj-border\": \"var(--border)\",\n                  \"--vj-border-subtle\": \"var(--border)\",\n                  \"--vj-accent\": \"var(--primary)\",\n                  \"--vj-accent-muted\": \"var(--muted)\",\n                  \"--vj-input-bg\": \"var(--secondary)\",\n                  \"--vj-input-border\": \"var(--border)\",\n                } as React.CSSProperties\n              }\n            />\n          ) : (\n            <div className=\"text-muted-foreground/50 p-3 text-sm font-mono\">\n              {\"// generate a spec to edit visually\"}\n            </div>\n          )\n        ) : activeTab === \"catalog\" ? (\n          <div className=\"h-full flex flex-col text-sm\">\n            <div className=\"flex items-center gap-3 px-3 h-9 border-b border-border\">\n              {(\n                [\n                  {\n                    key: \"components\",\n                    label: `components (${catalogData.components.length})`,\n                  },\n                  {\n                    key: \"actions\",\n                    label: `actions (${catalogData.actions.length})`,\n                  },\n                ] as const\n              ).map(({ key, label }) => (\n                <button\n                  key={key}\n                  onClick={() => setCatalogSection(key)}\n                  className={`text-xs font-mono transition-colors ${\n                    catalogSection === key\n                      ? \"text-foreground\"\n                      : \"text-muted-foreground hover:text-foreground\"\n                  }`}\n                >\n                  {label}\n                </button>\n              ))}\n            </div>\n            <div className=\"flex-1 overflow-auto p-3\">\n              {catalogSection === \"components\" ? (\n                <div className=\"space-y-3\">\n                  {catalogData.components.map((comp) => (\n                    <div\n                      key={comp.name}\n                      className=\"pb-3 border-b border-border last:border-b-0\"\n                    >\n                      <div className=\"flex items-baseline gap-2 mb-1\">\n                        <span className=\"font-mono font-medium text-foreground\">\n                          {comp.name}\n                        </span>\n                        {comp.slots.length > 0 && (\n                          <span className=\"text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground\">\n                            slots: {comp.slots.join(\", \")}\n                          </span>\n                        )}\n                      </div>\n                      {comp.description && (\n                        <p className=\"text-xs text-muted-foreground mb-2\">\n                          {comp.description}\n                        </p>\n                      )}\n                      {comp.props.length > 0 && (\n                        <div className=\"flex flex-wrap gap-1 mb-1\">\n                          {comp.props.map((p) => (\n                            <span\n                              key={p.name}\n                              className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                            >\n                              {p.name}\n                              <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                : {p.type}\n                              </span>\n                            </span>\n                          ))}\n                        </div>\n                      )}\n                      {comp.events.length > 0 && (\n                        <div className=\"flex flex-wrap gap-1 mt-1.5\">\n                          {comp.events.map((e) => (\n                            <span\n                              key={e}\n                              className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\"\n                            >\n                              on.{e}\n                            </span>\n                          ))}\n                        </div>\n                      )}\n                    </div>\n                  ))}\n                </div>\n              ) : (\n                <div className=\"space-y-3\">\n                  {catalogData.actions.map((action) => (\n                    <div\n                      key={action.name}\n                      className=\"pb-3 border-b border-border last:border-b-0\"\n                    >\n                      <span className=\"font-mono font-medium text-foreground\">\n                        {action.name}\n                      </span>\n                      {action.description && (\n                        <p className=\"text-xs text-muted-foreground mt-1 mb-2\">\n                          {action.description}\n                        </p>\n                      )}\n                      {action.params.length > 0 && (\n                        <div className=\"flex flex-wrap gap-1\">\n                          {action.params.map((p) => (\n                            <span\n                              key={p.name}\n                              className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                            >\n                              {p.name}\n                              <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                : {p.type}\n                              </span>\n                            </span>\n                          ))}\n                        </div>\n                      )}\n                    </div>\n                  ))}\n                </div>\n              )}\n            </div>\n          </div>\n        ) : activeTab === \"stream\" ? (\n          currentRawLines.length > 0 ? (\n            <CodeBlock\n              code={currentRawLines.join(\"\\n\")}\n              lang={streamLang}\n              fillHeight\n              hideCopyButton\n            />\n          ) : (\n            <div className=\"text-muted-foreground/50 p-3 text-sm font-mono\">\n              {isStreaming ? \"streaming...\" : \"// waiting for generation\"}\n            </div>\n          )\n        ) : activeTab === \"nested\" ? (\n          <CodeBlock code={nestedCode} lang=\"json\" fillHeight hideCopyButton />\n        ) : (\n          <CodeBlock\n            code={specCode}\n            lang={specLang}\n            fillHeight\n            hideCopyButton\n          />\n        )}\n      </div>\n    </div>\n  );\n\n  // Preview pane content\n  const previewPane = (\n    <div className=\"h-full flex flex-col border-t border-border\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        {(\n          [\n            { key: \"preview\", label: \"preview\" },\n            { key: \"code\", label: \"code\" },\n          ] as const\n        ).map(({ key, label }) => (\n          <button\n            key={key}\n            onClick={() => setRenderView(key)}\n            className={`text-xs font-mono transition-colors ${\n              renderView === key\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\"\n            }`}\n          >\n            {label}\n          </button>\n        ))}\n        <div className=\"flex-1\" />\n        {renderView === \"code\" && (\n          <CopyButton text={generatedCode} className=\"text-muted-foreground\" />\n        )}\n      </div>\n      <div className=\"flex-1 overflow-auto\">\n        {renderView === \"preview\" ? (\n          currentTree && currentTree.root ? (\n            <div className=\"w-full min-h-full flex items-center justify-center p-6\">\n              <PlaygroundRenderer\n                spec={currentTree}\n                data={currentTree.state}\n                loading={isStreaming}\n              />\n            </div>\n          ) : (\n            <div className=\"h-full flex items-center justify-center text-muted-foreground/50 text-sm\">\n              {isStreaming\n                ? \"generating...\"\n                : \"// enter a prompt to generate UI\"}\n            </div>\n          )\n        ) : (\n          <CodeBlock\n            code={generatedCode}\n            lang=\"tsx\"\n            fillHeight\n            hideCopyButton\n          />\n        )}\n      </div>\n    </div>\n  );\n\n  return (\n    <div className=\"h-full flex flex-col\">\n      <Header />\n\n      {/* Desktop: 3-pane resizable layout */}\n      <div className=\"hidden lg:flex flex-1 min-h-0\">\n        <ResizablePanelGroup className=\"flex-1\">\n          <ResizablePanel defaultSize={25} minSize={15}>\n            {chatPane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={35} minSize={20}>\n            {codePane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={40} minSize={20}>\n            {previewPane}\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      </div>\n\n      {/* Mobile: toolbar + content + prompt input */}\n      <div className=\"flex lg:hidden flex-col flex-1 min-h-0\">\n        {/* Top toolbar */}\n        <div className=\"border-b border-border px-3 h-9 flex items-center gap-3 shrink-0 overflow-x-auto\">\n          {/* Version badge */}\n          <button\n            onClick={() => setVersionsSheetOpen(true)}\n            className=\"text-xs font-mono font-medium px-1.5 py-0.5 rounded bg-muted text-foreground shrink-0\"\n          >\n            v\n            {versions.length > 0\n              ? versions.findIndex((v) => v.id === selectedVersionId) + 1 ||\n                versions.length\n              : 0}\n          </button>\n          {/* Code tabs */}\n          {([\"spec\", \"visual\", \"nested\", \"stream\", \"catalog\"] as const).map(\n            (tab) => (\n              <button\n                key={tab}\n                onClick={() => setMobileView(tab)}\n                className={`text-xs font-mono transition-colors shrink-0 ${\n                  mobileView === tab\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                }`}\n              >\n                {tab === \"spec\" ? (format === \"yaml\" ? \"yaml\" : \"json\") : tab}\n              </button>\n            ),\n          )}\n          <div className=\"flex-1\" />\n          {/* Preview / code toggle */}\n          {[\n            { key: \"preview\" as const, label: \"preview\" },\n            { key: \"generated-code\" as const, label: \"code\" },\n          ].map(({ key, label }) => (\n            <button\n              key={key}\n              onClick={() => setMobileView(key)}\n              className={`text-xs font-mono transition-colors shrink-0 ${\n                mobileView === key\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground hover:text-foreground\"\n              }`}\n            >\n              {label}\n            </button>\n          ))}\n        </div>\n\n        {/* Main content area */}\n        <div className=\"flex-1 min-h-0 overflow-auto\">\n          {mobileView === \"visual\" ? (\n            currentTree ? (\n              <JsonEditor\n                value={currentTree as unknown as JsonValue}\n                onChange={handleVisualChange}\n                readOnly={isStreaming}\n                sidebarOpen={false}\n                height=\"100%\"\n                className=\"h-full\"\n                style={\n                  {\n                    \"--vj-bg\": \"var(--background)\",\n                    \"--vj-bg-panel\": \"var(--background)\",\n                    \"--vj-bg-hover\": \"var(--muted)\",\n                    \"--vj-bg-selected\": \"var(--primary)\",\n                    \"--vj-bg-selected-muted\": \"var(--muted)\",\n                    \"--vj-text\": \"var(--foreground)\",\n                    \"--vj-text-selected\": \"var(--primary-foreground)\",\n                    \"--vj-text-muted\": \"var(--muted-foreground)\",\n                    \"--vj-text-dim\": \"var(--muted-foreground)\",\n                    \"--vj-border\": \"var(--border)\",\n                    \"--vj-border-subtle\": \"var(--border)\",\n                    \"--vj-accent\": \"var(--primary)\",\n                    \"--vj-accent-muted\": \"var(--muted)\",\n                    \"--vj-input-bg\": \"var(--secondary)\",\n                    \"--vj-input-border\": \"var(--border)\",\n                  } as React.CSSProperties\n                }\n              />\n            ) : (\n              <div className=\"text-muted-foreground/50 p-3 text-sm font-mono\">\n                {\"// generate a spec to edit visually\"}\n              </div>\n            )\n          ) : mobileView === \"catalog\" ? (\n            <div className=\"h-full flex flex-col text-sm\">\n              <div className=\"flex items-center gap-3 px-3 h-9 border-b border-border\">\n                {(\n                  [\n                    {\n                      key: \"components\",\n                      label: `components (${catalogData.components.length})`,\n                    },\n                    {\n                      key: \"actions\",\n                      label: `actions (${catalogData.actions.length})`,\n                    },\n                  ] as const\n                ).map(({ key, label }) => (\n                  <button\n                    key={key}\n                    onClick={() => setCatalogSection(key)}\n                    className={`text-xs font-mono transition-colors ${\n                      catalogSection === key\n                        ? \"text-foreground\"\n                        : \"text-muted-foreground hover:text-foreground\"\n                    }`}\n                  >\n                    {label}\n                  </button>\n                ))}\n              </div>\n              <div className=\"flex-1 overflow-auto p-3\">\n                {catalogSection === \"components\" ? (\n                  <div className=\"space-y-3\">\n                    {catalogData.components.map((comp) => (\n                      <div\n                        key={comp.name}\n                        className=\"pb-3 border-b border-border last:border-b-0\"\n                      >\n                        <div className=\"flex items-baseline gap-2 mb-1\">\n                          <span className=\"font-mono font-medium text-foreground\">\n                            {comp.name}\n                          </span>\n                          {comp.slots.length > 0 && (\n                            <span className=\"text-[10px] font-mono px-1.5 py-0.5 rounded bg-muted text-muted-foreground\">\n                              slots: {comp.slots.join(\", \")}\n                            </span>\n                          )}\n                        </div>\n                        {comp.description && (\n                          <p className=\"text-xs text-muted-foreground mb-2\">\n                            {comp.description}\n                          </p>\n                        )}\n                        {comp.props.length > 0 && (\n                          <div className=\"flex flex-wrap gap-1 mb-1\">\n                            {comp.props.map((p) => (\n                              <span\n                                key={p.name}\n                                className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                              >\n                                {p.name}\n                                <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                  : {p.type}\n                                </span>\n                              </span>\n                            ))}\n                          </div>\n                        )}\n                        {comp.events.length > 0 && (\n                          <div className=\"flex flex-wrap gap-1 mt-1.5\">\n                            {comp.events.map((e) => (\n                              <span\n                                key={e}\n                                className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400\"\n                              >\n                                on.{e}\n                              </span>\n                            ))}\n                          </div>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <div className=\"space-y-3\">\n                    {catalogData.actions.map((action) => (\n                      <div\n                        key={action.name}\n                        className=\"pb-3 border-b border-border last:border-b-0\"\n                      >\n                        <span className=\"font-mono font-medium text-foreground\">\n                          {action.name}\n                        </span>\n                        {action.description && (\n                          <p className=\"text-xs text-muted-foreground mt-1 mb-2\">\n                            {action.description}\n                          </p>\n                        )}\n                        {action.params.length > 0 && (\n                          <div className=\"flex flex-wrap gap-1\">\n                            {action.params.map((p) => (\n                              <span\n                                key={p.name}\n                                className=\"text-[11px] font-mono px-1.5 py-0.5 rounded bg-green-500/10 text-green-700 dark:text-green-400\"\n                              >\n                                {p.name}\n                                <span className=\"text-green-700/50 dark:text-green-400/50\">\n                                  : {p.type}\n                                </span>\n                              </span>\n                            ))}\n                          </div>\n                        )}\n                      </div>\n                    ))}\n                  </div>\n                )}\n              </div>\n            </div>\n          ) : mobileView === \"stream\" ? (\n            currentRawLines.length > 0 ? (\n              <CodeBlock\n                code={currentRawLines.join(\"\\n\")}\n                lang={streamLang}\n                fillHeight\n                hideCopyButton\n              />\n            ) : (\n              <div className=\"text-muted-foreground/50 p-3 text-sm font-mono\">\n                {isStreaming ? \"streaming...\" : \"// waiting for generation\"}\n              </div>\n            )\n          ) : mobileView === \"nested\" ? (\n            <CodeBlock\n              code={nestedCode}\n              lang=\"json\"\n              fillHeight\n              hideCopyButton\n            />\n          ) : mobileView === \"spec\" ? (\n            <CodeBlock\n              code={specCode}\n              lang={specLang}\n              fillHeight\n              hideCopyButton\n            />\n          ) : mobileView === \"preview\" ? (\n            currentTree && currentTree.root ? (\n              <div className=\"w-full min-h-full flex items-center justify-center p-6\">\n                <PlaygroundRenderer\n                  spec={currentTree}\n                  data={currentTree.state}\n                  loading={isStreaming}\n                />\n              </div>\n            ) : (\n              <div className=\"h-full flex flex-col items-center justify-center text-center px-4\">\n                {isStreaming ? (\n                  <p className=\"text-sm text-muted-foreground/50\">\n                    generating...\n                  </p>\n                ) : (\n                  <>\n                    <p className=\"text-sm text-muted-foreground mb-4\">\n                      Describe what you want to build, then iterate on it.\n                    </p>\n                    <div className=\"flex flex-wrap gap-2 justify-center\">\n                      {EXAMPLE_PROMPTS.map((prompt) => (\n                        <button\n                          key={prompt}\n                          onMouseDown={(e) => {\n                            e.preventDefault();\n                            flushSync(() => setInputValue(prompt));\n                            mobileInputRef.current?.focus();\n                            mobileInputRef.current?.setSelectionRange(\n                              prompt.length,\n                              prompt.length,\n                            );\n                          }}\n                          className=\"text-xs px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors\"\n                        >\n                          {prompt}\n                        </button>\n                      ))}\n                    </div>\n                  </>\n                )}\n              </div>\n            )\n          ) : (\n            /* generated-code */\n            <CodeBlock\n              code={generatedCode}\n              lang=\"tsx\"\n              fillHeight\n              hideCopyButton\n            />\n          )}\n        </div>\n\n        {/* Prompt input pinned to bottom */}\n        <div\n          className=\"border-t border-border p-3 shrink-0 cursor-text\"\n          onMouseDown={(e) => {\n            const target = e.target as HTMLElement;\n            if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n              e.preventDefault();\n              mobileInputRef.current?.focus();\n            }\n          }}\n        >\n          <textarea\n            ref={mobileInputRef}\n            value={inputValue}\n            onChange={(e) => setInputValue(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder=\"Describe changes...\"\n            className=\"w-full bg-background text-base resize-none outline-none placeholder:text-muted-foreground/50\"\n            rows={2}\n          />\n          <div className=\"flex justify-between items-center mt-2\">\n            <PlaygroundControls\n              format={format}\n              setFormat={setFormat}\n              editModes={editModes}\n              setEditModes={setEditModes}\n              showClear={versions.length > 0}\n              onClear={() => {\n                setVersions([]);\n                setSelectedVersionId(null);\n                clear();\n              }}\n            />\n            {isStreaming ? (\n              <button\n                onClick={() => clear()}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n                aria-label=\"Stop\"\n              >\n                <svg\n                  width=\"14\"\n                  height=\"14\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"currentColor\"\n                  stroke=\"none\"\n                >\n                  <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" />\n                </svg>\n              </button>\n            ) : (\n              <button\n                onClick={handleSubmit}\n                disabled={!inputValue.trim()}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n                aria-label=\"Send\"\n              >\n                <svg\n                  width=\"14\"\n                  height=\"14\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"M5 12h14\" />\n                  <path d=\"m12 5 7 7-7 7\" />\n                </svg>\n              </button>\n            )}\n          </div>\n        </div>\n\n        {/* Versions sheet */}\n        <Sheet open={versionsSheetOpen} onOpenChange={setVersionsSheetOpen}>\n          <SheetContent>\n            <SheetTitle className=\"text-sm font-mono mb-4\">Versions</SheetTitle>\n            <div className=\"flex-1 overflow-y-auto space-y-1\">\n              {versions.map((version, index) => (\n                <button\n                  key={version.id}\n                  onClick={() => {\n                    setSelectedVersionId(version.id);\n                    setVersionsSheetOpen(false);\n                  }}\n                  className={`w-full text-left px-3 py-2 rounded text-sm transition-colors ${\n                    selectedVersionId === version.id\n                      ? \"bg-muted text-foreground\"\n                      : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\"\n                  }`}\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-xs font-mono text-muted-foreground/70 shrink-0\">\n                      v{index + 1}\n                    </span>\n                    <span className=\"truncate flex-1\">{version.prompt}</span>\n                    {version.status === \"generating\" && (\n                      <span className=\"text-xs text-muted-foreground shrink-0 animate-pulse\">\n                        ...\n                      </span>\n                    )}\n                    {version.status === \"error\" && (\n                      <span className=\"text-xs text-red-500 shrink-0\">\n                        failed\n                      </span>\n                    )}\n                  </div>\n                  {version.usage && (\n                    <div className=\"mt-1 ml-6\">\n                      <span className=\"text-[10px] font-mono text-muted-foreground/60\">\n                        {formatTokens(\n                          version.usage.promptTokens -\n                            version.usage.cachedTokens,\n                        )}{\" \"}\n                        in · {formatTokens(version.usage.completionTokens)} out\n                        {version.usage.cachedTokens > 0\n                          ? ` · ${formatTokens(version.usage.cachedTokens)} cached`\n                          : \"\"}\n                      </span>\n                    </div>\n                  )}\n                </button>\n              ))}\n              {versions.length === 0 && (\n                <p className=\"text-sm text-muted-foreground px-3\">\n                  No versions yet. Enter a prompt to get started.\n                </p>\n              )}\n            </div>\n          </SheetContent>\n        </Sheet>\n      </div>\n\n      <Toaster position=\"bottom-right\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/search.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Dialog, DialogContent, DialogTitle } from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\n\ntype SearchResult = {\n  title: string;\n  href: string;\n  section: string;\n  snippet: string;\n};\n\nexport function Search() {\n  const router = useRouter();\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [results, setResults] = useState<SearchResult[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [activeIndex, setActiveIndex] = useState(0);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n  const abortRef = useRef<AbortController | null>(null);\n\n  const navigate = useCallback(\n    (href: string) => {\n      setOpen(false);\n      setQuery(\"\");\n      setResults([]);\n      router.push(href);\n    },\n    [router],\n  );\n\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n        e.preventDefault();\n        setOpen((prev) => !prev);\n      }\n    }\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  useEffect(() => {\n    if (open) {\n      setTimeout(() => inputRef.current?.focus(), 0);\n    } else {\n      setQuery(\"\");\n      setResults([]);\n    }\n  }, [open]);\n\n  useEffect(() => {\n    const q = query.trim();\n    if (!q) {\n      setResults([]);\n      setLoading(false);\n      return;\n    }\n\n    setLoading(true);\n    abortRef.current?.abort();\n    const controller = new AbortController();\n    abortRef.current = controller;\n\n    const timeout = setTimeout(async () => {\n      try {\n        const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {\n          signal: controller.signal,\n        });\n        if (res.ok) {\n          const data = await res.json();\n          setResults(data.results);\n        }\n      } catch {\n        // aborted or network error\n      } finally {\n        if (!controller.signal.aborted) {\n          setLoading(false);\n        }\n      }\n    }, 150);\n\n    return () => {\n      clearTimeout(timeout);\n      controller.abort();\n    };\n  }, [query]);\n\n  useEffect(() => {\n    setActiveIndex(0);\n  }, [results]);\n\n  function handleKeyDown(e: React.KeyboardEvent) {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.min(i + 1, results.length - 1));\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      setActiveIndex((i) => Math.max(i - 1, 0));\n    } else if (e.key === \"Enter\" && results[activeIndex]) {\n      e.preventDefault();\n      navigate(results[activeIndex].href);\n    }\n  }\n\n  useEffect(() => {\n    const active = listRef.current?.querySelector(\"[data-active='true']\");\n    active?.scrollIntoView({ block: \"nearest\" });\n  }, [activeIndex]);\n\n  const hasQuery = query.trim().length > 0;\n\n  return (\n    <>\n      <button\n        onClick={() => setOpen(true)}\n        className=\"hidden sm:flex items-center gap-2 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"11\" cy=\"11\" r=\"8\" />\n          <path d=\"m21 21-4.3-4.3\" />\n        </svg>\n        Search docs\n        <kbd className=\"pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground\">\n          <span>&#8984;</span>K\n        </kbd>\n      </button>\n\n      <button\n        onClick={() => setOpen(true)}\n        className=\"sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors\"\n        aria-label=\"Search docs\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"11\" cy=\"11\" r=\"8\" />\n          <path d=\"m21 21-4.3-4.3\" />\n        </svg>\n      </button>\n\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent\n          showCloseButton={false}\n          className=\"gap-0 p-0 sm:max-w-lg\"\n        >\n          <DialogTitle className=\"sr-only\">Search documentation</DialogTitle>\n          <div className=\"flex items-center gap-2 border-b px-3\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"16\"\n              height=\"16\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className=\"shrink-0 text-muted-foreground\"\n            >\n              <circle cx=\"11\" cy=\"11\" r=\"8\" />\n              <path d=\"m21 21-4.3-4.3\" />\n            </svg>\n            <input\n              ref={inputRef}\n              value={query}\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={handleKeyDown}\n              placeholder=\"Search docs...\"\n              className=\"flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground\"\n            />\n            {query && (\n              <button\n                onClick={() => setQuery(\"\")}\n                className=\"text-muted-foreground hover:text-foreground\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"14\"\n                  height=\"14\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <path d=\"M18 6 6 18\" />\n                  <path d=\"m6 6 12 12\" />\n                </svg>\n              </button>\n            )}\n          </div>\n\n          <div\n            ref={listRef}\n            className=\"max-h-[min(60vh,400px)] overflow-y-auto p-2\"\n          >\n            {loading && hasQuery ? (\n              <div className=\"flex items-center justify-center py-6\">\n                <div className=\"h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent\" />\n              </div>\n            ) : hasQuery && results.length === 0 ? (\n              <p className=\"py-6 text-center text-sm text-muted-foreground\">\n                No results found.\n              </p>\n            ) : !hasQuery ? (\n              <p className=\"py-6 text-center text-sm text-muted-foreground\">\n                Type to search documentation...\n              </p>\n            ) : (\n              results.map((item, i) => (\n                <button\n                  key={item.href}\n                  data-active={i === activeIndex}\n                  onClick={() => navigate(item.href)}\n                  onMouseEnter={() => setActiveIndex(i)}\n                  className={cn(\n                    \"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors\",\n                    i === activeIndex\n                      ? \"bg-accent text-accent-foreground\"\n                      : \"text-foreground\",\n                  )}\n                >\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"text-sm font-medium\">{item.title}</span>\n                    <span className=\"shrink-0 text-xs text-muted-foreground\">\n                      {item.section}\n                    </span>\n                  </div>\n                  {item.snippet && (\n                    <span className=\"line-clamp-2 text-xs text-muted-foreground leading-relaxed\">\n                      {item.snippet}\n                    </span>\n                  )}\n                </button>\n              ))\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/table-of-contents.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/utils\";\n\ntype Heading = {\n  id: string;\n  text: string;\n  level: number;\n};\n\nfunction getHeadings(): Heading[] {\n  const article = document.querySelector(\"article\");\n  if (!article) return [];\n  const elements = article.querySelectorAll(\"h2[id], h3[id]\");\n  return Array.from(elements).map((el) => ({\n    id: el.id,\n    text: el.textContent?.replace(/#$/, \"\").trim() ?? \"\",\n    level: el.tagName === \"H3\" ? 3 : 2,\n  }));\n}\n\nexport function TableOfContents() {\n  const pathname = usePathname();\n  const [headings, setHeadings] = useState<Heading[]>([]);\n  const [activeId, setActiveId] = useState<string>(\"\");\n\n  useEffect(() => {\n    const timer = setTimeout(() => setHeadings(getHeadings()), 100);\n    return () => clearTimeout(timer);\n  }, [pathname]);\n\n  useEffect(() => {\n    if (headings.length === 0) return;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            setActiveId(entry.target.id);\n          }\n        }\n      },\n      { rootMargin: \"0px 0px -75% 0px\", threshold: 0.1 },\n    );\n\n    for (const h of headings) {\n      const el = document.getElementById(h.id);\n      if (el) observer.observe(el);\n    }\n\n    return () => observer.disconnect();\n  }, [headings]);\n\n  if (headings.length === 0) return null;\n\n  return (\n    <nav aria-label=\"On this page\">\n      <h4 className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3\">\n        On this page\n      </h4>\n      <ul className=\"space-y-1\">\n        {headings.map((h) => (\n          <li key={h.id}>\n            <a\n              href={`#${h.id}`}\n              className={cn(\n                \"block text-xs leading-relaxed py-0.5 transition-colors\",\n                h.level === 3 && \"pl-3\",\n                activeId === h.id\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground hover:text-foreground\",\n              )}\n            >\n              {h.text}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n  return (\n    <NextThemesProvider\n      attribute=\"class\"\n      defaultTheme=\"dark\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      {children}\n    </NextThemesProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return <div className=\"w-8 h-8\" />;\n  }\n\n  return (\n    <button\n      onClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n      className=\"w-8 h-8 flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\n      aria-label=\"Toggle theme\"\n    >\n      {theme === \"dark\" ? (\n        <svg\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"12\" cy=\"12\" r=\"4\" />\n          <path d=\"M12 2v2\" />\n          <path d=\"M12 20v2\" />\n          <path d=\"m4.93 4.93 1.41 1.41\" />\n          <path d=\"m17.66 17.66 1.41 1.41\" />\n          <path d=\"M2 12h2\" />\n          <path d=\"M20 12h2\" />\n          <path d=\"m6.34 17.66-1.41 1.41\" />\n          <path d=\"m19.07 4.93-1.41 1.41\" />\n        </svg>\n      ) : (\n        <svg\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n      {...props}\n    >\n      <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "apps/web/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "apps/web/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant,\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "apps/web/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/web/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "apps/web/components/ui/carousel.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-carousel-react\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\");\n  }\n\n  return context;\n}\n\nfunction Carousel({\n  orientation = \"horizontal\",\n  opts,\n  setApi,\n  plugins,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n  const [carouselRef, api] = useEmblaCarousel(\n    {\n      ...opts,\n      axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n    },\n    plugins,\n  );\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n  const onSelect = React.useCallback((api: CarouselApi) => {\n    if (!api) return;\n    setCanScrollPrev(api.canScrollPrev());\n    setCanScrollNext(api.canScrollNext());\n  }, []);\n\n  const scrollPrev = React.useCallback(() => {\n    api?.scrollPrev();\n  }, [api]);\n\n  const scrollNext = React.useCallback(() => {\n    api?.scrollNext();\n  }, [api]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        scrollPrev();\n      } else if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [scrollPrev, scrollNext],\n  );\n\n  React.useEffect(() => {\n    if (!api || !setApi) return;\n    setApi(api);\n  }, [api, setApi]);\n\n  React.useEffect(() => {\n    if (!api) return;\n    onSelect(api);\n    api.on(\"reInit\", onSelect);\n    api.on(\"select\", onSelect);\n\n    return () => {\n      api?.off(\"select\", onSelect);\n    };\n  }, [api, onSelect]);\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        carouselRef,\n        api: api,\n        opts,\n        orientation:\n          orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n      }}\n    >\n      <div\n        onKeyDownCapture={handleKeyDown}\n        className={cn(\"relative\", className)}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        data-slot=\"carousel\"\n        {...props}\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { carouselRef, orientation } = useCarousel();\n\n  return (\n    <div\n      ref={carouselRef}\n      className=\"overflow-hidden\"\n      data-slot=\"carousel-content\"\n    >\n      <div\n        className={cn(\n          \"flex\",\n          orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { orientation } = useCarousel();\n\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      data-slot=\"carousel-item\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full\",\n        orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CarouselPrevious({\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-previous\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute size-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 -left-12 -translate-y-1/2\"\n          : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      {...props}\n    >\n      <ArrowLeft />\n      <span className=\"sr-only\">Previous slide</span>\n    </Button>\n  );\n}\n\nfunction CarouselNext({\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-next\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute size-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 -right-12 -translate-y-1/2\"\n          : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      {...props}\n    >\n      <ArrowRight />\n      <span className=\"sr-only\">Next slide</span>\n    </Button>\n  );\n}\n\nexport {\n  type CarouselApi,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n};\n"
  },
  {
    "path": "apps/web/components/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"grid place-content-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox };\n"
  },
  {
    "path": "apps/web/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\n\nfunction Collapsible({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n  return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />;\n}\n\nfunction CollapsibleTrigger({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleTrigger\n      data-slot=\"collapsible-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction CollapsibleContent({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleContent\n      data-slot=\"collapsible-content\"\n      {...props}\n    />\n  );\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "apps/web/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogFooter({\n  className,\n  showCloseButton = false,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {showCloseButton && (\n        <DialogPrimitive.Close asChild>\n          <Button variant=\"outline\">Close</Button>\n        </DialogPrimitive.Close>\n      )}\n    </div>\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "apps/web/components/ui/drawer.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Drawer({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n  return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />;\n}\n\nfunction DrawerTrigger({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return (\n    <DrawerPrimitive.Overlay\n      data-slot=\"drawer-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  return (\n    <DrawerPortal data-slot=\"drawer-portal\">\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        data-slot=\"drawer-content\"\n        className={cn(\n          \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n          \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n          \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n          \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n        {children}\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  );\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\n        \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return (\n    <DrawerPrimitive.Title\n      data-slot=\"drawer-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return (\n    <DrawerPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "apps/web/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "apps/web/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "apps/web/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "apps/web/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MoreHorizontalIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants, type Button } from \"@/components/ui/button\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n  return (\n    <nav\n      role=\"navigation\"\n      aria-label=\"pagination\"\n      data-slot=\"pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationContent({\n  className,\n  ...props\n}: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"pagination-content\"\n      className={cn(\"flex flex-row items-center gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n  return <li data-slot=\"pagination-item\" {...props} />;\n}\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nfunction PaginationLink({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) {\n  return (\n    <a\n      aria-current={isActive ? \"page\" : undefined}\n      data-slot=\"pagination-link\"\n      data-active={isActive}\n      className={cn(\n        buttonVariants({\n          variant: isActive ? \"outline\" : \"ghost\",\n          size,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationPrevious({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to previous page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pl-2.5\", className)}\n      {...props}\n    >\n      <ChevronLeftIcon />\n      <span className=\"hidden sm:block\">Previous</span>\n    </PaginationLink>\n  );\n}\n\nfunction PaginationNext({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to next page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pr-2.5\", className)}\n      {...props}\n    >\n      <span className=\"hidden sm:block\">Next</span>\n      <ChevronRightIcon />\n    </PaginationLink>\n  );\n}\n\nfunction PaginationEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"pagination-ellipsis\"\n      className={cn(\"flex size-9 items-center justify-center\", className)}\n      {...props}\n    >\n      <MoreHorizontalIcon className=\"size-4\" />\n      <span className=\"sr-only\">More pages</span>\n    </span>\n  );\n}\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n"
  },
  {
    "path": "apps/web/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"popover-header\"\n      className={cn(\"flex flex-col gap-1 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  return (\n    <div\n      data-slot=\"popover-title\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"popover-description\"\n      className={cn(\"text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverAnchor,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverDescription,\n};\n"
  },
  {
    "path": "apps/web/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Progress({\n  className,\n  value,\n  ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\n        \"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator\n        data-slot=\"progress-indicator\"\n        className=\"bg-primary h-full w-full flex-1 transition-all\"\n        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n      />\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport { Progress };\n"
  },
  {
    "path": "apps/web/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CircleIcon } from \"lucide-react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "apps/web/components/ui/resizable.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { GripVerticalIcon } from \"lucide-react\";\nimport { Group, Panel, Separator } from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ResizablePanelGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof Group>) {\n  return (\n    <Group\n      data-slot=\"resizable-panel-group\"\n      className={cn(\n        \"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ResizablePanel({ ...props }: React.ComponentProps<typeof Panel>) {\n  return <Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({\n  withHandle,\n  className,\n  ...props\n}: React.ComponentProps<typeof Separator> & {\n  withHandle?: boolean;\n}) {\n  return (\n    <Separator\n      data-slot=\"resizable-handle\"\n      className={cn(\n        \"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      {withHandle && (\n        <div className=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n          <GripVerticalIcon className=\"size-2.5\" />\n        </div>\n      )}\n    </Separator>\n  );\n}\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle };\n"
  },
  {
    "path": "apps/web/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"item-aligned\",\n  align = \"center\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        align={align}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        data-slot=\"select-item-indicator\"\n        className=\"absolute right-2 flex size-3.5 items-center justify-center\"\n      >\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "apps/web/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "apps/web/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cn } from \"@/lib/utils\";\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = React.forwardRef<\n  React.ComponentRef<typeof SheetPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/75 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:duration-150 data-[state=open]:duration-150\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetSideVariants = {\n  left: \"inset-y-0 left-0 h-full w-3/4 max-w-xs border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left\",\n  right:\n    \"inset-y-0 right-0 h-full w-3/4 max-w-xs data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right\",\n};\n\nconst SheetContent = React.forwardRef<\n  React.ComponentRef<typeof SheetPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {\n    side?: \"left\" | \"right\";\n    overlayClassName?: string;\n  }\n>(({ className, children, side = \"left\", overlayClassName, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay className={overlayClassName} />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-150 data-[state=open]:duration-150 data-[state=open]:animate-in data-[state=closed]:animate-out focus:outline-none\",\n        sheetSideVariants[side],\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </SheetPrimitive.Content>\n  </SheetPortal>\n));\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetTitle = React.forwardRef<\n  React.ComponentRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold text-foreground\", className)}\n    {...props}\n  />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nexport { Sheet, SheetTrigger, SheetClose, SheetContent, SheetTitle };\n"
  },
  {
    "path": "apps/web/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "apps/web/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Slider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n  const _values = React.useMemo(\n    () =>\n      Array.isArray(value)\n        ? value\n        : Array.isArray(defaultValue)\n          ? defaultValue\n          : [min, max],\n    [value, defaultValue, min, max],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      defaultValue={defaultValue}\n      value={value}\n      min={min}\n      max={max}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track\n        data-slot=\"slider-track\"\n        className={cn(\n          \"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n        )}\n      >\n        <SliderPrimitive.Range\n          data-slot=\"slider-range\"\n          className={cn(\n            \"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n          )}\n        />\n      </SliderPrimitive.Track>\n      {Array.from({ length: _values.length }, (_, index) => (\n        <SliderPrimitive.Thumb\n          data-slot=\"slider-thumb\"\n          key={index}\n          className=\"border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\"\n        />\n      ))}\n    </SliderPrimitive.Root>\n  );\n}\n\nexport { Slider };\n"
  },
  {
    "path": "apps/web/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport {\n  CircleCheckIcon,\n  InfoIcon,\n  Loader2Icon,\n  OctagonXIcon,\n  TriangleAlertIcon,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner, type ToasterProps } from \"sonner\";\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      icons={{\n        success: <CircleCheckIcon className=\"size-4\" />,\n        info: <InfoIcon className=\"size-4\" />,\n        warning: <TriangleAlertIcon className=\"size-4\" />,\n        error: <OctagonXIcon className=\"size-4\" />,\n        loading: <Loader2Icon className=\"size-4 animate-spin\" />,\n      }}\n      style={\n        {\n          \"--normal-bg\": \"var(--popover)\",\n          \"--normal-text\": \"var(--popover-foreground)\",\n          \"--normal-border\": \"var(--border)\",\n          \"--border-radius\": \"var(--radius)\",\n        } as React.CSSProperties\n      }\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "apps/web/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Switch({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      data-size={size}\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "apps/web/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "apps/web/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      className={cn(\"flex flex-col gap-2\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsList({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      className={cn(\n        \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "apps/web/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/web/components/ui/toggle-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type VariantProps } from \"class-variance-authority\";\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\nimport { toggleVariants } from \"@/components/ui/toggle\";\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }\n>({\n  size: \"default\",\n  variant: \"default\",\n  spacing: 0,\n});\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  spacing = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }) {\n  return (\n    <ToggleGroupPrimitive.Root\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      data-spacing={spacing}\n      style={{ \"--gap\": spacing } as React.CSSProperties}\n      className={cn(\n        \"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider value={{ variant, size, spacing }}>\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive.Root>\n  );\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n  VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      data-spacing={context.spacing}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\",\n        \"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n}\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "apps/web/components/ui/toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "apps/web/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />;\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "apps/web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "apps/web/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      // Disable prop-types - we use TypeScript for type checking\n      \"react/prop-types\": \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "apps/web/lib/docs-navigation.ts",
    "content": "export type NavItem = {\n  title: string;\n  href: string;\n  external?: boolean;\n};\n\nexport type NavSection = {\n  title: string;\n  items: NavItem[];\n};\n\nexport const docsNavigation: NavSection[] = [\n  {\n    title: \"Getting Started\",\n    items: [\n      { title: \"Introduction\", href: \"/docs\" },\n      { title: \"Installation\", href: \"/docs/installation\" },\n      { title: \"Quick Start\", href: \"/docs/quick-start\" },\n      { title: \"Skills\", href: \"/docs/skills\" },\n      { title: \"Migration Guide\", href: \"/docs/migration\" },\n      { title: \"Changelog\", href: \"/docs/changelog\" },\n    ],\n  },\n  {\n    title: \"Core\",\n    items: [\n      { title: \"Specs\", href: \"/docs/specs\" },\n      { title: \"Schemas\", href: \"/docs/schemas\" },\n      { title: \"Catalog\", href: \"/docs/catalog\" },\n      { title: \"Data Binding\", href: \"/docs/data-binding\" },\n      { title: \"Computed Values\", href: \"/docs/computed-values\" },\n      { title: \"Visibility\", href: \"/docs/visibility\" },\n      { title: \"Watchers\", href: \"/docs/watchers\" },\n      { title: \"Validation\", href: \"/docs/validation\" },\n    ],\n  },\n  {\n    title: \"Rendering\",\n    items: [\n      { title: \"Renderers\", href: \"/docs/renderers\" },\n      { title: \"Registry\", href: \"/docs/registry\" },\n      { title: \"Streaming\", href: \"/docs/streaming\" },\n      { title: \"Generation Modes\", href: \"/docs/generation-modes\" },\n    ],\n  },\n  {\n    title: \"Examples\",\n    items: [{ title: \"Browse All Examples\", href: \"/examples\" }],\n  },\n  {\n    title: \"Guides\",\n    items: [\n      { title: \"Custom Schema\", href: \"/docs/custom-schema\" },\n      { title: \"Code Export\", href: \"/docs/code-export\" },\n    ],\n  },\n  {\n    title: \"Integrations\",\n    items: [\n      { title: \"AI SDK\", href: \"/docs/ai-sdk\" },\n      { title: \"A2UI\", href: \"/docs/a2ui\" },\n      { title: \"Adaptive Cards\", href: \"/docs/adaptive-cards\" },\n      { title: \"AG-UI\", href: \"/docs/ag-ui\" },\n      { title: \"OpenAPI\", href: \"/docs/openapi\" },\n    ],\n  },\n  {\n    title: \"API Reference\",\n    items: [\n      { title: \"@json-render/core\", href: \"/docs/api/core\" },\n      { title: \"@json-render/react\", href: \"/docs/api/react\" },\n      { title: \"@json-render/react-pdf\", href: \"/docs/api/react-pdf\" },\n      { title: \"@json-render/react-email\", href: \"/docs/api/react-email\" },\n      { title: \"@json-render/shadcn\", href: \"/docs/api/shadcn\" },\n      { title: \"@json-render/react-native\", href: \"/docs/api/react-native\" },\n      { title: \"@json-render/image\", href: \"/docs/api/image\" },\n      { title: \"@json-render/remotion\", href: \"/docs/api/remotion\" },\n      { title: \"@json-render/vue\", href: \"/docs/api/vue\" },\n      { title: \"@json-render/svelte\", href: \"/docs/api/svelte\" },\n      { title: \"@json-render/solid\", href: \"/docs/api/solid\" },\n      {\n        title: \"@json-render/react-three-fiber\",\n        href: \"/docs/api/react-three-fiber\",\n      },\n      { title: \"@json-render/codegen\", href: \"/docs/api/codegen\" },\n      { title: \"@json-render/mcp\", href: \"/docs/api/mcp\" },\n      { title: \"@json-render/redux\", href: \"/docs/api/redux\" },\n      { title: \"@json-render/zustand\", href: \"/docs/api/zustand\" },\n      { title: \"@json-render/jotai\", href: \"/docs/api/jotai\" },\n      { title: \"@json-render/xstate\", href: \"/docs/api/xstate\" },\n      { title: \"@json-render/yaml\", href: \"/docs/api/yaml\" },\n    ],\n  },\n];\n\n// Flatten all pages for current page lookup (excludes external links)\nexport const allDocsPages = docsNavigation.flatMap((section) =>\n  section.items.filter((item) => !item.external),\n);\n"
  },
  {
    "path": "apps/web/lib/examples.ts",
    "content": "export type Example = {\n  slug: string;\n  title: string;\n  description: string;\n  tags: string[];\n  githubPath: string;\n  demoUrl?: string;\n};\n\nconst GITHUB_BASE =\n  \"https://github.com/vercel-labs/json-render/tree/main/examples\";\n\nexport const examples: Example[] = [\n  {\n    slug: \"chat\",\n    title: \"Chat\",\n    description:\n      \"AI chat app with tool calling, streaming UI, and rich components powered by the AI SDK.\",\n    tags: [\"React\", \"Next.js\", \"AI\"],\n    githubPath: \"examples/chat\",\n    demoUrl: \"https://chat-demo.json-render.dev\",\n  },\n  {\n    slug: \"dashboard\",\n    title: \"Dashboard\",\n    description:\n      \"AI-generated dashboard with drag-and-drop, charts, and real-time data binding.\",\n    tags: [\"React\", \"Next.js\", \"AI\"],\n    githubPath: \"examples/dashboard\",\n    demoUrl: \"https://dashboard-demo.json-render.dev\",\n  },\n  {\n    slug: \"no-ai\",\n    title: \"No AI\",\n    description:\n      \"Static specs rendered without any AI — forms, cards, tables, and more from hardcoded JSON.\",\n    tags: [\"React\", \"Next.js\"],\n    githubPath: \"examples/no-ai\",\n    demoUrl: \"https://no-ai-demo.json-render.dev\",\n  },\n  {\n    slug: \"svelte\",\n    title: \"Svelte\",\n    description:\n      \"Svelte renderer demo with counter, todo list, and two-way data binding.\",\n    tags: [\"Svelte\", \"Vite\"],\n    githubPath: \"examples/svelte\",\n    demoUrl: \"https://svelte-demo.json-render.dev\",\n  },\n  {\n    slug: \"svelte-chat\",\n    title: \"Svelte Chat\",\n    description: \"AI chat app built with SvelteKit and the Svelte renderer.\",\n    tags: [\"Svelte\", \"SvelteKit\", \"AI\"],\n    githubPath: \"examples/svelte-chat\",\n    demoUrl: \"https://json-render-svelte-chat-demo.labs.vercel.dev\",\n  },\n  {\n    slug: \"vue\",\n    title: \"Vue\",\n    description:\n      \"Vue renderer demo with counter, todo list, and two-way data binding.\",\n    tags: [\"Vue\", \"Vite\"],\n    githubPath: \"examples/vue\",\n    demoUrl: \"https://vue-demo.json-render.dev\",\n  },\n  {\n    slug: \"solid\",\n    title: \"Solid\",\n    description:\n      \"Solid renderer demo with counter, todo list, and two-way data binding.\",\n    tags: [\"Solid\", \"Vite\"],\n    githubPath: \"examples/solid\",\n    demoUrl: \"https://solid-demo.json-render.dev\",\n  },\n  {\n    slug: \"vite-renderers\",\n    title: \"Multi-Framework Renderers\",\n    description:\n      \"Same spec rendered with React, Vue, Svelte, and Solid side by side — hot-swappable at runtime.\",\n    tags: [\"React\", \"Vue\", \"Svelte\", \"Solid\", \"Vite\"],\n    githubPath: \"examples/vite-renderers\",\n  },\n  {\n    slug: \"react-email\",\n    title: \"React Email\",\n    description:\n      \"Generate HTML and plain-text emails from json-render specs using React Email.\",\n    tags: [\"React\", \"Email\"],\n    githubPath: \"examples/react-email\",\n    demoUrl: \"https://react-email-demo.json-render.dev\",\n  },\n  {\n    slug: \"react-pdf\",\n    title: \"React PDF\",\n    description:\n      \"Generate PDF documents from json-render specs with @react-pdf/renderer.\",\n    tags: [\"React\", \"PDF\"],\n    githubPath: \"examples/react-pdf\",\n    demoUrl: \"https://react-pdf-demo.json-render.dev\",\n  },\n  {\n    slug: \"react-three-fiber\",\n    title: \"React Three Fiber\",\n    description:\n      \"3D scenes generated from json-render specs using Three.js and React Three Fiber.\",\n    tags: [\"React\", \"3D\"],\n    githubPath: \"examples/react-three-fiber\",\n    demoUrl: \"https://react-three-fiber-demo.json-render.dev\",\n  },\n  {\n    slug: \"react-native\",\n    title: \"React Native\",\n    description:\n      \"Mobile app rendering json-render specs with Expo and React Native.\",\n    tags: [\"React Native\", \"Expo\"],\n    githubPath: \"examples/react-native\",\n  },\n  {\n    slug: \"remotion\",\n    title: \"Remotion\",\n    description: \"Generate videos from json-render specs using Remotion.\",\n    tags: [\"React\", \"Video\"],\n    githubPath: \"examples/remotion\",\n    demoUrl: \"https://remotion-demo.json-render.dev\",\n  },\n  {\n    slug: \"image\",\n    title: \"Image\",\n    description:\n      \"Generate OG images and social cards from json-render specs using Satori.\",\n    tags: [\"React\", \"Image\"],\n    githubPath: \"examples/image\",\n    demoUrl: \"https://image-demo.json-render.dev\",\n  },\n  {\n    slug: \"mcp\",\n    title: \"MCP App\",\n    description:\n      \"MCP server that serves shadcn UIs to Claude, ChatGPT, Cursor, and VS Code.\",\n    tags: [\"React\", \"MCP\", \"Vite\"],\n    githubPath: \"examples/mcp\",\n  },\n];\n\nexport const allTags = Array.from(\n  new Set(examples.flatMap((e) => e.tags)),\n).sort();\n\nexport function getGitHubUrl(example: Example): string {\n  return `${GITHUB_BASE}/${example.slug}`;\n}\n"
  },
  {
    "path": "apps/web/lib/mdx-to-markdown.ts",
    "content": "/**\n * Converts raw MDX content to clean Markdown suitable for AI agents.\n *\n * Transformations:\n * - Remove `export` statements (metadata, etc.)\n * - Remove `import` statements\n * - Replace `<PackageInstall packages=\"x y\" />` with a fenced bash code block\n * - Strip standalone JSX callout divs (the amber concept boxes)\n * - Pass everything else through as-is (already valid Markdown)\n */\nexport function mdxToCleanMarkdown(raw: string): string {\n  const lines = raw.split(\"\\n\");\n  const out: string[] = [];\n  let inJsxBlock = false;\n  let jsxDepth = 0;\n\n  for (const line of lines) {\n    const trimmed = line.trim();\n\n    // Skip export and import statements\n    if (trimmed.startsWith(\"export \") || trimmed.startsWith(\"import \")) {\n      continue;\n    }\n\n    // Handle PackageInstall component\n    const pkgMatch = trimmed.match(\n      /<PackageInstall\\s+packages=\"([^\"]+)\"\\s*\\/>/,\n    );\n    if (pkgMatch) {\n      const packages = pkgMatch[1];\n      out.push(\"```bash\");\n      out.push(`pnpm add ${packages}`);\n      out.push(\"```\");\n      out.push(\"\");\n      continue;\n    }\n\n    // Track JSX blocks (like the callout divs) and skip them\n    if (\n      !inJsxBlock &&\n      trimmed.startsWith(\"<div \") &&\n      trimmed.includes(\"className=\")\n    ) {\n      inJsxBlock = true;\n      jsxDepth = 1;\n      continue;\n    }\n\n    if (inJsxBlock) {\n      // Count opening/closing div tags to handle nesting\n      const opens = (line.match(/<div[\\s>]/g) || []).length;\n      const closes = (line.match(/<\\/div>/g) || []).length;\n      jsxDepth += opens - closes;\n      if (jsxDepth <= 0) {\n        inJsxBlock = false;\n        jsxDepth = 0;\n      }\n      continue;\n    }\n\n    out.push(line);\n  }\n\n  // Clean up leading blank lines\n  let result = out.join(\"\\n\");\n  result = result.replace(/^\\n+/, \"\\n\").trim();\n  return result;\n}\n"
  },
  {
    "path": "apps/web/lib/page-metadata.ts",
    "content": "import type { Metadata } from \"next\";\nimport { PAGE_TITLES } from \"./page-titles\";\n\nconst DESCRIPTION =\n  \"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.\";\n\nexport function pageMetadata(slug: string): Metadata {\n  const title = PAGE_TITLES[slug];\n  if (!title) return {};\n\n  const displayTitle = title.replace(/\\n/g, \" \");\n  const fullTitle = `${displayTitle} | json-render`;\n  const ogImageUrl = slug ? `/og/${slug}` : \"/og\";\n\n  return {\n    title: displayTitle,\n    openGraph: {\n      type: \"website\",\n      locale: \"en_US\",\n      siteName: \"json-render\",\n      title: fullTitle,\n      description: DESCRIPTION,\n      images: [\n        {\n          url: ogImageUrl,\n          width: 1200,\n          height: 630,\n          alt: `${displayTitle} - json-render`,\n        },\n      ],\n    },\n    twitter: {\n      card: \"summary_large_image\",\n      title: fullTitle,\n      description: DESCRIPTION,\n      images: [ogImageUrl],\n    },\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/page-titles.ts",
    "content": "/**\n * Single source of truth for page titles.\n * Used by both page metadata exports and the OG image route.\n *\n * Keys mirror the page's URL path (e.g., \"docs/changelog\" → /og/docs/changelog).\n * Values are display titles (without the \"| json-render\" suffix — the layout template adds that).\n */\nexport const PAGE_TITLES: Record<string, string> = {\n  // Home (no slug)\n  \"\": \"The Generative UI\\nFramework\",\n\n  // Top-level\n  playground: \"Playground\",\n  examples: \"Examples\",\n\n  // Docs\n  docs: \"Introduction\",\n  \"docs/quick-start\": \"Quick Start\",\n  \"docs/installation\": \"Installation\",\n  \"docs/catalog\": \"Catalog\",\n  \"docs/schemas\": \"Schemas\",\n  \"docs/specs\": \"Specs\",\n  \"docs/registry\": \"Registry\",\n  \"docs/streaming\": \"Streaming\",\n  \"docs/validation\": \"Validation\",\n  \"docs/data-binding\": \"Data Binding\",\n  \"docs/computed-values\": \"Computed Values\",\n  \"docs/visibility\": \"Visibility\",\n  \"docs/watchers\": \"Watchers\",\n  \"docs/renderers\": \"Renderers\",\n  \"docs/generation-modes\": \"Generation Modes\",\n  \"docs/code-export\": \"Code Export\",\n  \"docs/custom-schema\": \"Custom Schema & Renderer\",\n  \"docs/ai-sdk\": \"AI SDK Integration\",\n  \"docs/adaptive-cards\": \"Adaptive Cards Integration\",\n  \"docs/openapi\": \"OpenAPI Integration\",\n  \"docs/a2ui\": \"A2UI Integration\",\n  \"docs/ag-ui\": \"AG-UI Integration\",\n  \"docs/migration\": \"Migration Guide\",\n  \"docs/changelog\": \"Changelog\",\n  \"docs/skills\": \"Skills\",\n\n  // API references\n  \"docs/api/core\": \"@json-render/core API\",\n  \"docs/api/react\": \"@json-render/react API\",\n  \"docs/api/vue\": \"@json-render/vue API\",\n  \"docs/api/solid\": \"@json-render/solid API\",\n  \"docs/api/react-pdf\": \"@json-render/react-pdf API\",\n  \"docs/api/react-email\": \"@json-render/react-email API\",\n  \"docs/api/react-native\": \"@json-render/react-native API\",\n  \"docs/api/svelte\": \"@json-render/svelte API\",\n  \"docs/api/codegen\": \"@json-render/codegen API\",\n  \"docs/api/image\": \"@json-render/image API\",\n  \"docs/api/remotion\": \"@json-render/remotion API\",\n  \"docs/api/shadcn\": \"@json-render/shadcn API\",\n  \"docs/api/mcp\": \"@json-render/mcp API\",\n  \"docs/api/redux\": \"@json-render/redux API\",\n  \"docs/api/zustand\": \"@json-render/zustand API\",\n  \"docs/api/jotai\": \"@json-render/jotai API\",\n  \"docs/api/react-three-fiber\": \"@json-render/react-three-fiber API\",\n  \"docs/api/xstate\": \"@json-render/xstate API\",\n  \"docs/api/yaml\": \"@json-render/yaml API\",\n};\n\n/**\n * Get the page title for a given slug.\n * Returns null if the slug is not in the whitelist.\n */\nexport function getPageTitle(slug: string): string | null {\n  return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;\n}\n"
  },
  {
    "path": "apps/web/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "apps/web/lib/render/catalog-display.ts",
    "content": "/**\n * Shared utility for extracting catalog data for display in the UI.\n * Used by both the demo and playground components.\n */\n\nexport interface CatalogField {\n  name: string;\n  type: string;\n}\n\nexport interface CatalogComponentInfo {\n  name: string;\n  description: string;\n  props: CatalogField[];\n  slots: string[];\n  events: string[];\n}\n\nexport interface CatalogActionInfo {\n  name: string;\n  description: string;\n  params: CatalogField[];\n}\n\nexport interface CatalogDisplayData {\n  components: CatalogComponentInfo[];\n  actions: CatalogActionInfo[];\n}\n\n/**\n * Extract field names and types from a Zod schema object.\n * Supports both Zod v3 and v4 shape formats.\n */\nexport function extractFields(zodObj: unknown): CatalogField[] {\n  if (!zodObj) return [];\n  try {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const obj = zodObj as any;\n    // Zod v4: shape is a plain object; Zod v3: shape is via _def.shape()\n    const shape =\n      typeof obj.shape === \"object\"\n        ? obj.shape\n        : typeof obj._def?.shape === \"function\"\n          ? obj._def.shape()\n          : typeof obj._def?.shape === \"object\"\n            ? obj._def.shape\n            : null;\n    if (!shape) return [];\n\n    return Object.entries(shape).map(([name, schema]) => {\n      let type = \"unknown\";\n      try {\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const s = schema as any;\n        const typeName: string = s?._zod?.def?.type ?? s?._def?.typeName ?? \"\";\n        if (typeName.includes(\"string\")) type = \"string\";\n        else if (typeName.includes(\"number\")) type = \"number\";\n        else if (typeName.includes(\"boolean\")) type = \"boolean\";\n        else if (typeName.includes(\"array\")) type = \"array\";\n        else if (typeName.includes(\"enum\")) {\n          const values = s?._zod?.def?.values ?? s?._def?.values;\n          type = Array.isArray(values) ? values.join(\" | \") : \"enum\";\n        } else if (typeName.includes(\"union\")) type = \"union\";\n        else if (typeName.includes(\"nullable\")) {\n          const inner = s?._zod?.def?.innerType ?? s?._def?.innerType;\n          const innerName: string =\n            inner?._zod?.def?.type ?? inner?._def?.typeName ?? \"\";\n          if (innerName.includes(\"string\")) type = \"string?\";\n          else if (innerName.includes(\"number\")) type = \"number?\";\n          else if (innerName.includes(\"boolean\")) type = \"boolean?\";\n          else if (innerName.includes(\"array\")) type = \"array?\";\n          else if (innerName.includes(\"enum\")) {\n            const values = inner?._zod?.def?.values ?? inner?._def?.values;\n            type = Array.isArray(values) ? `(${values.join(\" | \")})?` : \"enum?\";\n          } else type = \"optional\";\n        }\n      } catch {\n        // ignore\n      }\n      return { name, type };\n    });\n  } catch {\n    return [];\n  }\n}\n\n/**\n * Extract display data from a catalog's raw data.\n * Parses component definitions and action definitions into a\n * structured format suitable for rendering in the UI.\n */\nexport function buildCatalogDisplayData(\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  rawCatalogData: any,\n): CatalogDisplayData {\n  const components = Object.entries(rawCatalogData.components ?? {})\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    .map(([name, def]: [string, any]) => ({\n      name,\n      description: (def.description as string) ?? \"\",\n      props: extractFields(def.props),\n      slots: (def.slots as string[]) ?? [],\n      events: (def.events as string[]) ?? [],\n    }))\n    .sort((a, b) => a.name.localeCompare(b.name));\n\n  const actions = Object.entries(rawCatalogData.actions ?? {})\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    .map(([name, def]: [string, any]) => ({\n      name,\n      description: (def.description as string) ?? \"\",\n      params: extractFields(def.params),\n    }))\n    .sort((a, b) => a.name.localeCompare(b.name));\n\n  return { components, actions };\n}\n"
  },
  {
    "path": "apps/web/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\n/**\n * Web playground component catalog\n *\n * This defines the components available for AI generation in the playground.\n * Components and actions are implemented in lib/registry.tsx via defineRegistry.\n *\n * Keep schemas simple — one format per prop, no unions.\n * Fewer components = less confusion for the AI.\n */\nexport const playgroundCatalog = defineCatalog(schema, {\n  components: {\n    // ── Layout ──────────────────────────────────────────────────────────\n    Card: {\n      props: z.object({\n        title: z.string().nullable(),\n        description: z.string().nullable(),\n        maxWidth: z.enum([\"sm\", \"md\", \"lg\", \"full\"]).nullable(),\n        centered: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Container card for content sections. Use for forms/content boxes, NOT for page headers.\",\n      example: { title: \"Overview\", description: \"Your account summary\" },\n    },\n\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n        gap: z.enum([\"none\", \"sm\", \"md\", \"lg\"]).nullable(),\n        align: z.enum([\"start\", \"center\", \"end\", \"stretch\"]).nullable(),\n        justify: z\n          .enum([\"start\", \"center\", \"end\", \"between\", \"around\"])\n          .nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Flex container for layouts\",\n      example: { direction: \"vertical\", gap: \"md\" },\n    },\n\n    Grid: {\n      props: z.object({\n        columns: z.number().nullable(),\n        gap: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Grid layout (1-6 columns)\",\n      example: { columns: 3, gap: \"md\" },\n    },\n\n    Separator: {\n      props: z.object({\n        orientation: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n      }),\n      description: \"Visual separator line\",\n    },\n\n    Tabs: {\n      props: z.object({\n        tabs: z.array(\n          z.object({\n            label: z.string(),\n            value: z.string(),\n          }),\n        ),\n        defaultValue: z.string().nullable(),\n        value: z.string().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Tab navigation. Use { $bindState } on value for active tab binding.\",\n    },\n\n    Accordion: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            title: z.string(),\n            content: z.string(),\n          }),\n        ),\n        type: z.enum([\"single\", \"multiple\"]).nullable(),\n      }),\n      description:\n        \"Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.\",\n    },\n\n    Collapsible: {\n      props: z.object({\n        title: z.string(),\n        defaultOpen: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Collapsible section with trigger. Children render inside.\",\n    },\n\n    Dialog: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n        openPath: z.string(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Modal dialog. Set openPath to a boolean state path. Use setState to toggle.\",\n    },\n\n    Drawer: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n        openPath: z.string(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.\",\n    },\n\n    Carousel: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            title: z.string().nullable(),\n            description: z.string().nullable(),\n          }),\n        ),\n      }),\n      description: \"Horizontally scrollable carousel of cards.\",\n    },\n\n    // ── Data Display ────────────────────────────────────────────────────\n    Table: {\n      props: z.object({\n        columns: z.array(z.string()),\n        rows: z.array(z.array(z.string())),\n        caption: z.string().nullable(),\n      }),\n      description:\n        'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [[\"Alice\",\"admin\"],[\"Bob\",\"user\"]].',\n      example: {\n        columns: [\"Name\", \"Role\"],\n        rows: [\n          [\"Alice\", \"Admin\"],\n          [\"Bob\", \"User\"],\n        ],\n      },\n    },\n\n    Heading: {\n      props: z.object({\n        text: z.string(),\n        level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      }),\n      description: \"Heading text (h1-h4)\",\n      example: { text: \"Welcome\", level: \"h1\" },\n    },\n\n    Text: {\n      props: z.object({\n        text: z.string(),\n        variant: z\n          .enum([\"body\", \"caption\", \"muted\", \"lead\", \"code\"])\n          .nullable(),\n      }),\n      description: \"Paragraph text\",\n      example: { text: \"Hello, world!\" },\n    },\n\n    Image: {\n      props: z.object({\n        alt: z.string(),\n        width: z.number().nullable(),\n        height: z.number().nullable(),\n      }),\n      description: \"Placeholder image (displays alt text in a styled box)\",\n    },\n\n    Avatar: {\n      props: z.object({\n        src: z.string().nullable(),\n        name: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      }),\n      description: \"User avatar with fallback initials\",\n      example: { name: \"Jane Doe\", size: \"md\" },\n    },\n\n    Badge: {\n      props: z.object({\n        text: z.string(),\n        variant: z.enum([\"default\", \"success\", \"warning\", \"danger\"]).nullable(),\n      }),\n      description: \"Status badge\",\n      example: { text: \"Active\", variant: \"success\" },\n    },\n\n    Alert: {\n      props: z.object({\n        title: z.string(),\n        message: z.string().nullable(),\n        type: z.enum([\"info\", \"success\", \"warning\", \"error\"]).nullable(),\n      }),\n      description: \"Alert banner\",\n      example: {\n        title: \"Note\",\n        message: \"Your changes have been saved.\",\n        type: \"success\",\n      },\n    },\n\n    Progress: {\n      props: z.object({\n        value: z.number(),\n        max: z.number().nullable(),\n        label: z.string().nullable(),\n      }),\n      description: \"Progress bar (value 0-100)\",\n      example: { value: 65, max: 100, label: \"Upload progress\" },\n    },\n\n    Skeleton: {\n      props: z.object({\n        width: z.string().nullable(),\n        height: z.string().nullable(),\n        rounded: z.boolean().nullable(),\n      }),\n      description: \"Loading placeholder skeleton\",\n    },\n\n    Spinner: {\n      props: z.object({\n        size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n        label: z.string().nullable(),\n      }),\n      description: \"Loading spinner indicator\",\n    },\n\n    Tooltip: {\n      props: z.object({\n        content: z.string(),\n        text: z.string(),\n      }),\n      description: \"Hover tooltip. Shows content on hover over text.\",\n    },\n\n    Popover: {\n      props: z.object({\n        trigger: z.string(),\n        content: z.string(),\n      }),\n      description: \"Popover that appears on click of trigger.\",\n    },\n\n    Rating: {\n      props: z.object({\n        value: z.number(),\n        max: z.number().nullable(),\n        label: z.string().nullable(),\n      }),\n      description: \"Star rating display\",\n    },\n\n    // ── Charts ──────────────────────────────────────────────────────────\n    BarGraph: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(\n          z.object({\n            label: z.string(),\n            value: z.number(),\n          }),\n        ),\n      }),\n      description: \"Vertical bar chart\",\n    },\n\n    LineGraph: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(\n          z.object({\n            label: z.string(),\n            value: z.number(),\n          }),\n        ),\n      }),\n      description: \"Line chart with points\",\n    },\n\n    // ── Form Inputs ─────────────────────────────────────────────────────\n    Input: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        type: z.enum([\"text\", \"email\", \"password\", \"number\"]).nullable(),\n        placeholder: z.string().nullable(),\n        value: z.string().nullable(),\n        checks: z\n          .array(\n            z.object({\n              type: z.string(),\n              message: z.string(),\n              args: z.record(z.string(), z.unknown()).optional(),\n            }),\n          )\n          .nullable(),\n      }),\n      events: [\"submit\", \"focus\", \"blur\"],\n      description:\n        \"Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength).\",\n      example: {\n        label: \"Email\",\n        name: \"email\",\n        type: \"email\",\n        placeholder: \"you@example.com\",\n      },\n    },\n\n    Textarea: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        placeholder: z.string().nullable(),\n        rows: z.number().nullable(),\n        value: z.string().nullable(),\n        checks: z\n          .array(\n            z.object({\n              type: z.string(),\n              message: z.string(),\n              args: z.record(z.string(), z.unknown()).optional(),\n            }),\n          )\n          .nullable(),\n      }),\n      description:\n        \"Multi-line text input. Use { $bindState } on value for binding. Use checks for validation.\",\n    },\n\n    Select: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        options: z.array(z.string()),\n        placeholder: z.string().nullable(),\n        value: z.string().nullable(),\n        checks: z\n          .array(\n            z.object({\n              type: z.string(),\n              message: z.string(),\n              args: z.record(z.string(), z.unknown()).optional(),\n            }),\n          )\n          .nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Dropdown select input. Use { $bindState } on value for binding. Use checks for validation.\",\n    },\n\n    Checkbox: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        checked: z.boolean().nullable(),\n      }),\n      events: [\"change\"],\n      description: \"Checkbox input. Use { $bindState } on checked for binding.\",\n    },\n\n    Radio: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        options: z.array(z.string()),\n        value: z.string().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Radio button group. Use { $bindState } on value for binding.\",\n    },\n\n    Switch: {\n      props: z.object({\n        label: z.string(),\n        name: z.string(),\n        checked: z.boolean().nullable(),\n      }),\n      events: [\"change\"],\n      description: \"Toggle switch. Use { $bindState } on checked for binding.\",\n    },\n\n    Slider: {\n      props: z.object({\n        label: z.string().nullable(),\n        min: z.number().nullable(),\n        max: z.number().nullable(),\n        step: z.number().nullable(),\n        value: z.number().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Range slider input. Use { $bindState } on value for binding.\",\n    },\n\n    // ── Actions ─────────────────────────────────────────────────────────\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      events: [\"press\"],\n      description: \"Clickable button. Bind on.press for handler.\",\n      example: { label: \"Submit\", variant: \"primary\" },\n    },\n\n    Link: {\n      props: z.object({\n        label: z.string(),\n        href: z.string(),\n      }),\n      events: [\"press\"],\n      description: \"Anchor link. Bind on.press for click handler.\",\n    },\n\n    DropdownMenu: {\n      props: z.object({\n        label: z.string(),\n        items: z.array(\n          z.object({\n            label: z.string(),\n            value: z.string(),\n          }),\n        ),\n      }),\n      events: [\"select\"],\n      description: \"Dropdown menu with trigger button and selectable items.\",\n    },\n\n    Toggle: {\n      props: z.object({\n        label: z.string(),\n        pressed: z.boolean().nullable(),\n        variant: z.enum([\"default\", \"outline\"]).nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Toggle button. Use { $bindState } on pressed for state binding.\",\n    },\n\n    ToggleGroup: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            label: z.string(),\n            value: z.string(),\n          }),\n        ),\n        type: z.enum([\"single\", \"multiple\"]).nullable(),\n        value: z.string().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.\",\n    },\n\n    ButtonGroup: {\n      props: z.object({\n        buttons: z.array(\n          z.object({\n            label: z.string(),\n            value: z.string(),\n          }),\n        ),\n        selected: z.string().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Segmented button group. Use { $bindState } on selected for selected value.\",\n    },\n\n    Pagination: {\n      props: z.object({\n        totalPages: z.number(),\n        page: z.number().nullable(),\n      }),\n      events: [\"change\"],\n      description:\n        \"Page navigation. Use { $bindState } on page for current page number.\",\n    },\n  },\n\n  actions: {\n    setState: {\n      params: z.object({\n        statePath: z.string(),\n        value: z.unknown(),\n      }),\n      description: \"Update a value in the state model at the given statePath.\",\n    },\n\n    pushState: {\n      params: z.object({\n        statePath: z.string(),\n        value: z.unknown(),\n        clearStatePath: z.string().optional(),\n      }),\n      description:\n        'Append an item to an array in state. Value can contain {\"$state\":\"/statePath\"} refs and \"$id\" for auto IDs. clearStatePath resets another path after pushing.',\n    },\n\n    removeState: {\n      params: z.object({\n        statePath: z.string(),\n        index: z.number(),\n      }),\n      description: \"Remove an item from an array in state at the given index.\",\n    },\n\n    buttonClick: {\n      params: z.object({\n        message: z.string().nullable(),\n      }),\n      description: \"Shows a toast with the message.\",\n    },\n\n    formSubmit: {\n      params: z.object({\n        formName: z.string().nullable(),\n      }),\n      description: \"Shows a toast confirming form submission.\",\n    },\n\n    linkClick: {\n      params: z.object({\n        href: z.string(),\n      }),\n      description: \"Shows a toast with the link destination.\",\n    },\n  },\n});\n"
  },
  {
    "path": "apps/web/lib/render/registry.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n  defineRegistry,\n  useBoundProp,\n  useStateBinding,\n  useFieldValidation,\n} from \"@json-render/react\";\nimport { toast } from \"sonner\";\n\nimport { playgroundCatalog } from \"./catalog\";\n\n// shadcn components\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Alert, AlertTitle, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  Dialog as DialogPrimitive,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n  Accordion as AccordionPrimitive,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Carousel as CarouselPrimitive,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport {\n  Table as TablePrimitive,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport {\n  Drawer as DrawerPrimitive,\n  DrawerContent,\n  DrawerDescription,\n  DrawerHeader,\n  DrawerTitle,\n} from \"@/components/ui/drawer\";\nimport {\n  DropdownMenu as DropdownMenuPrimitive,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Pagination as PaginationPrimitive,\n  PaginationContent,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n} from \"@/components/ui/pagination\";\nimport {\n  Popover as PopoverPrimitive,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  Tabs as TabsPrimitive,\n  TabsList,\n  TabsTrigger,\n} from \"@/components/ui/tabs\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nimport {\n  Tooltip as TooltipPrimitive,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\n// =============================================================================\n// Registry — components + actions, types inferred from catalog\n// =============================================================================\n\nexport const { registry, executeAction } = defineRegistry(playgroundCatalog, {\n  components: {\n    // ── Layout ────────────────────────────────────────────────────────\n\n    Card: ({ props, children }) => {\n      const maxWidthClass =\n        props.maxWidth === \"sm\"\n          ? \"max-w-xs sm:min-w-[280px]\"\n          : props.maxWidth === \"md\"\n            ? \"max-w-sm sm:min-w-[320px]\"\n            : props.maxWidth === \"lg\"\n              ? \"max-w-md sm:min-w-[360px]\"\n              : \"w-full\";\n      const centeredClass = props.centered ? \"mx-auto\" : \"\";\n\n      return (\n        <div\n          className={`border border-border rounded-lg p-4 bg-card text-card-foreground overflow-hidden ${maxWidthClass} ${centeredClass}`}\n        >\n          {(props.title || props.description) && (\n            <div className=\"mb-4\">\n              {props.title && (\n                <h3 className=\"font-semibold text-lg text-left\">\n                  {props.title}\n                </h3>\n              )}\n              {props.description && (\n                <p className=\"text-sm text-muted-foreground mt-1 text-left\">\n                  {props.description}\n                </p>\n              )}\n            </div>\n          )}\n          <div className=\"space-y-3\">{children}</div>\n        </div>\n      );\n    },\n\n    Stack: ({ props, children }) => {\n      const isHorizontal = props.direction === \"horizontal\";\n      const gapClass =\n        props.gap === \"lg\"\n          ? \"gap-4\"\n          : props.gap === \"md\"\n            ? \"gap-3\"\n            : props.gap === \"sm\"\n              ? \"gap-2\"\n              : props.gap === \"none\"\n                ? \"gap-0\"\n                : \"gap-3\";\n      const alignClass =\n        props.align === \"center\"\n          ? \"items-center\"\n          : props.align === \"end\"\n            ? \"items-end\"\n            : props.align === \"stretch\"\n              ? \"items-stretch\"\n              : \"items-start\";\n      const justifyClass =\n        props.justify === \"center\"\n          ? \"justify-center\"\n          : props.justify === \"end\"\n            ? \"justify-end\"\n            : props.justify === \"between\"\n              ? \"justify-between\"\n              : props.justify === \"around\"\n                ? \"justify-around\"\n                : \"\";\n\n      return (\n        <div\n          className={`flex ${isHorizontal ? \"flex-row flex-wrap\" : \"flex-col\"} ${gapClass} ${alignClass} ${justifyClass}`}\n        >\n          {children}\n        </div>\n      );\n    },\n\n    Grid: ({ props, children }) => {\n      const n = props.columns ?? 1;\n      const cols =\n        n >= 6\n          ? \"grid-cols-6\"\n          : n >= 5\n            ? \"grid-cols-5\"\n            : n >= 4\n              ? \"grid-cols-4\"\n              : n >= 3\n                ? \"grid-cols-3\"\n                : n >= 2\n                  ? \"grid-cols-2\"\n                  : \"grid-cols-1\";\n      const gridGap =\n        props.gap === \"lg\" ? \"gap-4\" : props.gap === \"sm\" ? \"gap-2\" : \"gap-3\";\n\n      return <div className={`grid ${cols} ${gridGap}`}>{children}</div>;\n    },\n\n    Separator: ({ props }) => (\n      <Separator\n        orientation={props.orientation ?? \"horizontal\"}\n        className={props.orientation === \"vertical\" ? \"h-full mx-2\" : \"my-3\"}\n      />\n    ),\n\n    Tabs: ({ props, bindings, emit }) => {\n      const tabs = props.tabs ?? [];\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(\n        props.defaultValue ?? tabs[0]?.value ?? \"\",\n      );\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? tabs[0]?.value ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      return (\n        <TabsPrimitive\n          value={value}\n          onValueChange={(v) => {\n            setValue(v);\n            emit(\"change\");\n          }}\n        >\n          <TabsList>\n            {tabs.map((tab) => (\n              <TabsTrigger key={tab.value} value={tab.value}>\n                {tab.label}\n              </TabsTrigger>\n            ))}\n          </TabsList>\n        </TabsPrimitive>\n      );\n    },\n\n    Accordion: ({ props }) => {\n      const items = props.items ?? [];\n      const accordionType = props.type ?? \"single\";\n\n      if (accordionType === \"multiple\") {\n        return (\n          <AccordionPrimitive type=\"multiple\" className=\"w-full\">\n            {items.map((item, i) => (\n              <AccordionItem key={i} value={`item-${i}`}>\n                <AccordionTrigger>{item.title}</AccordionTrigger>\n                <AccordionContent>{item.content}</AccordionContent>\n              </AccordionItem>\n            ))}\n          </AccordionPrimitive>\n        );\n      }\n      return (\n        <AccordionPrimitive type=\"single\" collapsible className=\"w-full\">\n          {items.map((item, i) => (\n            <AccordionItem key={i} value={`item-${i}`}>\n              <AccordionTrigger>{item.title}</AccordionTrigger>\n              <AccordionContent>{item.content}</AccordionContent>\n            </AccordionItem>\n          ))}\n        </AccordionPrimitive>\n      );\n    },\n\n    Collapsible: ({ props, children }) => {\n      const [open, setOpen] = useState(props.defaultOpen ?? false);\n      return (\n        <Collapsible open={open} onOpenChange={setOpen} className=\"w-full\">\n          <CollapsibleTrigger asChild>\n            <button className=\"flex w-full items-center justify-between rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\">\n              {props.title}\n              <svg\n                className={`h-4 w-4 transition-transform ${open ? \"rotate-180\" : \"\"}`}\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n                strokeWidth={2}\n              >\n                <path\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  d=\"M19 9l-7 7-7-7\"\n                />\n              </svg>\n            </button>\n          </CollapsibleTrigger>\n          <CollapsibleContent className=\"pt-2\">{children}</CollapsibleContent>\n        </Collapsible>\n      );\n    },\n\n    Dialog: ({ props, children }) => {\n      const [open, setOpen] = useStateBinding<boolean>(props.openPath);\n      return (\n        <DialogPrimitive open={open ?? false} onOpenChange={(v) => setOpen(v)}>\n          <DialogContent>\n            <DialogHeader>\n              <DialogTitle>{props.title}</DialogTitle>\n              {props.description && (\n                <DialogDescription>{props.description}</DialogDescription>\n              )}\n            </DialogHeader>\n            {children}\n          </DialogContent>\n        </DialogPrimitive>\n      );\n    },\n\n    Drawer: ({ props, children }) => {\n      const [open, setOpen] = useStateBinding<boolean>(props.openPath);\n      return (\n        <DrawerPrimitive open={open ?? false} onOpenChange={(v) => setOpen(v)}>\n          <DrawerContent>\n            <DrawerHeader>\n              <DrawerTitle>{props.title}</DrawerTitle>\n              {props.description && (\n                <DrawerDescription>{props.description}</DrawerDescription>\n              )}\n            </DrawerHeader>\n            <div className=\"p-4\">{children}</div>\n          </DrawerContent>\n        </DrawerPrimitive>\n      );\n    },\n\n    Carousel: ({ props }) => {\n      const items = props.items ?? [];\n      return (\n        <CarouselPrimitive className=\"w-full\">\n          <CarouselContent>\n            {items.map((item, i) => (\n              <CarouselItem\n                key={i}\n                className=\"basis-3/4 md:basis-1/2 lg:basis-1/3\"\n              >\n                <div className=\"border border-border rounded-lg p-4 bg-card h-full\">\n                  {item.title && (\n                    <h4 className=\"font-semibold text-sm mb-1\">{item.title}</h4>\n                  )}\n                  {item.description && (\n                    <p className=\"text-sm text-muted-foreground\">\n                      {item.description}\n                    </p>\n                  )}\n                </div>\n              </CarouselItem>\n            ))}\n          </CarouselContent>\n          <CarouselPrevious />\n          <CarouselNext />\n        </CarouselPrimitive>\n      );\n    },\n\n    // ── Data Display ──────────────────────────────────────────────────\n\n    Table: ({ props }) => {\n      const columns = props.columns ?? [];\n      const rawRows: unknown[] = Array.isArray(props.rows) ? props.rows : [];\n\n      const rows = rawRows.map((row) => {\n        if (Array.isArray(row)) return row.map(String);\n        if (row && typeof row === \"object\") {\n          const obj = row as Record<string, unknown>;\n          return columns.map((col) =>\n            String(obj[col] ?? obj[col.toLowerCase()] ?? \"\"),\n          );\n        }\n        return columns.map(() => \"\");\n      });\n\n      return (\n        <div className=\"rounded-md border border-border overflow-hidden\">\n          <TablePrimitive>\n            {props.caption && <TableCaption>{props.caption}</TableCaption>}\n            <TableHeader>\n              <TableRow>\n                {columns.map((col) => (\n                  <TableHead key={col}>{col}</TableHead>\n                ))}\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {rows.map((row, i) => (\n                <TableRow key={i}>\n                  {row.map((cell, j) => (\n                    <TableCell key={j}>{cell}</TableCell>\n                  ))}\n                </TableRow>\n              ))}\n            </TableBody>\n          </TablePrimitive>\n        </div>\n      );\n    },\n\n    Heading: ({ props }) => {\n      const level = props.level ?? \"h2\";\n      const headingClass =\n        level === \"h1\"\n          ? \"text-2xl font-bold\"\n          : level === \"h3\"\n            ? \"text-base font-semibold\"\n            : level === \"h4\"\n              ? \"text-sm font-semibold\"\n              : \"text-lg font-semibold\";\n\n      if (level === \"h1\")\n        return <h1 className={`${headingClass} text-left`}>{props.text}</h1>;\n      if (level === \"h3\")\n        return <h3 className={`${headingClass} text-left`}>{props.text}</h3>;\n      if (level === \"h4\")\n        return <h4 className={`${headingClass} text-left`}>{props.text}</h4>;\n      return <h2 className={`${headingClass} text-left`}>{props.text}</h2>;\n    },\n\n    Text: ({ props }) => {\n      const textClass =\n        props.variant === \"caption\"\n          ? \"text-xs\"\n          : props.variant === \"muted\"\n            ? \"text-sm text-muted-foreground\"\n            : props.variant === \"lead\"\n              ? \"text-xl text-muted-foreground\"\n              : props.variant === \"code\"\n                ? \"font-mono text-sm bg-muted px-1.5 py-0.5 rounded\"\n                : \"text-sm\";\n\n      if (props.variant === \"code\") {\n        return <code className={`${textClass} text-left`}>{props.text}</code>;\n      }\n      return <p className={`${textClass} text-left`}>{props.text}</p>;\n    },\n\n    Image: ({ props }) => (\n      <div\n        className=\"bg-muted border border-border rounded flex items-center justify-center text-xs text-muted-foreground aspect-video\"\n        style={{ width: props.width ?? 80, height: props.height ?? 60 }}\n      >\n        {props.alt || \"img\"}\n      </div>\n    ),\n\n    Avatar: ({ props }) => {\n      const name = props.name || \"?\";\n      const initials = name\n        .split(\" \")\n        .map((n) => n[0])\n        .join(\"\")\n        .slice(0, 2)\n        .toUpperCase();\n      const avatarSize =\n        props.size === \"lg\"\n          ? \"w-12 h-12 text-base\"\n          : props.size === \"sm\"\n            ? \"w-8 h-8 text-xs\"\n            : \"w-10 h-10 text-sm\";\n\n      return (\n        <div\n          className={`${avatarSize} rounded-full bg-muted flex items-center justify-center font-medium`}\n        >\n          {initials}\n        </div>\n      );\n    },\n\n    Badge: ({ props }) => {\n      const variant =\n        props.variant === \"success\" || props.variant === \"warning\"\n          ? \"secondary\"\n          : props.variant === \"danger\"\n            ? \"destructive\"\n            : \"default\";\n      const customClass =\n        props.variant === \"success\"\n          ? \"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100\"\n          : props.variant === \"warning\"\n            ? \"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100\"\n            : \"\";\n\n      return (\n        <Badge variant={variant} className={customClass}>\n          {props.text}\n        </Badge>\n      );\n    },\n\n    Alert: ({ props }) => {\n      const variant = props.type === \"error\" ? \"destructive\" : \"default\";\n      const customClass =\n        props.type === \"success\"\n          ? \"border-green-200 bg-green-50 text-green-900 dark:border-green-800 dark:bg-green-950 dark:text-green-100\"\n          : props.type === \"warning\"\n            ? \"border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100\"\n            : props.type === \"info\"\n              ? \"border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100\"\n              : \"\";\n\n      return (\n        <Alert variant={variant} className={customClass}>\n          <AlertTitle>{props.title}</AlertTitle>\n          {props.message && (\n            <AlertDescription>{props.message}</AlertDescription>\n          )}\n        </Alert>\n      );\n    },\n\n    Progress: ({ props }) => {\n      const value = Math.min(100, Math.max(0, props.value || 0));\n      return (\n        <div className=\"space-y-2\">\n          {props.label && (\n            <Label className=\"text-sm text-muted-foreground\">\n              {props.label}\n            </Label>\n          )}\n          <Progress value={value} />\n        </div>\n      );\n    },\n\n    Skeleton: ({ props }) => (\n      <Skeleton\n        className={props.rounded ? \"rounded-full\" : \"rounded-md\"}\n        style={{\n          width: props.width ?? \"100%\",\n          height: props.height ?? \"1.25rem\",\n        }}\n      />\n    ),\n\n    Spinner: ({ props }) => {\n      const sizeClass =\n        props.size === \"lg\"\n          ? \"h-8 w-8\"\n          : props.size === \"sm\"\n            ? \"h-4 w-4\"\n            : \"h-6 w-6\";\n      return (\n        <div className=\"flex items-center gap-2\">\n          <svg\n            className={`${sizeClass} animate-spin text-muted-foreground`}\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n          >\n            <circle\n              className=\"opacity-25\"\n              cx=\"12\"\n              cy=\"12\"\n              r=\"10\"\n              stroke=\"currentColor\"\n              strokeWidth=\"4\"\n            />\n            <path\n              className=\"opacity-75\"\n              fill=\"currentColor\"\n              d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"\n            />\n          </svg>\n          {props.label && (\n            <span className=\"text-sm text-muted-foreground\">{props.label}</span>\n          )}\n        </div>\n      );\n    },\n\n    Tooltip: ({ props }) => (\n      <TooltipProvider>\n        <TooltipPrimitive>\n          <TooltipTrigger asChild>\n            <span className=\"text-sm underline decoration-dotted cursor-help\">\n              {props.text}\n            </span>\n          </TooltipTrigger>\n          <TooltipContent>\n            <p>{props.content}</p>\n          </TooltipContent>\n        </TooltipPrimitive>\n      </TooltipProvider>\n    ),\n\n    Popover: ({ props }) => (\n      <PopoverPrimitive>\n        <PopoverTrigger asChild>\n          <Button variant=\"outline\" className=\"text-sm\">\n            {props.trigger}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-64\">\n          <p className=\"text-sm\">{props.content}</p>\n        </PopoverContent>\n      </PopoverPrimitive>\n    ),\n\n    Rating: ({ props }) => {\n      const ratingValue = props.value || 0;\n      const maxRating = props.max ?? 5;\n      return (\n        <div className=\"space-y-2\">\n          {props.label && (\n            <Label className=\"text-sm text-muted-foreground\">\n              {props.label}\n            </Label>\n          )}\n          <div className=\"flex gap-1\">\n            {Array.from({ length: maxRating }).map((_, i) => (\n              <span\n                key={i}\n                className={`text-lg ${i < ratingValue ? \"text-yellow-400\" : \"text-muted\"}`}\n              >\n                *\n              </span>\n            ))}\n          </div>\n        </div>\n      );\n    },\n\n    // ── Charts ────────────────────────────────────────────────────────\n\n    BarGraph: ({ props }) => {\n      const data = props.data || [];\n      const maxValue = Math.max(...data.map((d) => d.value), 1);\n\n      return (\n        <div className=\"space-y-2\">\n          {props.title && (\n            <div className=\"text-sm font-medium text-left\">{props.title}</div>\n          )}\n          <div className=\"flex gap-2\">\n            {data.map((d, i) => (\n              <div key={i} className=\"flex-1 flex flex-col items-center gap-1\">\n                <div className=\"text-xs text-muted-foreground\">{d.value}</div>\n                <div className=\"w-full h-24 flex items-end\">\n                  <div\n                    className=\"w-full bg-primary rounded-t transition-all\"\n                    style={{\n                      height: `${(d.value / maxValue) * 100}%`,\n                      minHeight: 2,\n                    }}\n                  />\n                </div>\n                <div className=\"text-xs text-muted-foreground truncate w-full text-center\">\n                  {d.label}\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      );\n    },\n\n    LineGraph: ({ props }) => {\n      const data = props.data || [];\n      const maxValue = Math.max(...data.map((d) => d.value));\n      const minValue = Math.min(...data.map((d) => d.value));\n      const range = maxValue - minValue || 1;\n\n      const width = 300;\n      const height = 100;\n      const padding = { top: 10, right: 10, bottom: 10, left: 10 };\n      const chartWidth = width - padding.left - padding.right;\n      const chartHeight = height - padding.top - padding.bottom;\n\n      const points = data.map((d, i) => {\n        const x =\n          padding.left +\n          (data.length > 1\n            ? (i / (data.length - 1)) * chartWidth\n            : chartWidth / 2);\n        const y =\n          padding.top +\n          chartHeight -\n          ((d.value - minValue) / range) * chartHeight;\n        return { x, y, ...d };\n      });\n\n      const pathD =\n        points.length > 0\n          ? `M ${points.map((p) => `${p.x} ${p.y}`).join(\" L \")}`\n          : \"\";\n\n      return (\n        <div className=\"space-y-2\">\n          {props.title && (\n            <div className=\"text-sm font-medium text-left\">{props.title}</div>\n          )}\n          <div className=\"relative h-28\">\n            <svg viewBox={`0 0 ${width} ${height}`} className=\"w-full h-full\">\n              <line\n                x1={padding.left}\n                y1={padding.top + chartHeight / 2}\n                x2={width - padding.right}\n                y2={padding.top + chartHeight / 2}\n                stroke=\"currentColor\"\n                strokeOpacity=\"0.1\"\n                strokeWidth=\"1\"\n              />\n              <line\n                x1={padding.left}\n                y1={padding.top}\n                x2={width - padding.right}\n                y2={padding.top}\n                stroke=\"currentColor\"\n                strokeOpacity=\"0.1\"\n                strokeWidth=\"1\"\n              />\n              <line\n                x1={padding.left}\n                y1={height - padding.bottom}\n                x2={width - padding.right}\n                y2={height - padding.bottom}\n                stroke=\"currentColor\"\n                strokeOpacity=\"0.1\"\n                strokeWidth=\"1\"\n              />\n              {pathD && (\n                <path\n                  d={pathD}\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  className=\"text-primary\"\n                />\n              )}\n              {points.map((p, i) => (\n                <circle\n                  key={i}\n                  cx={p.x}\n                  cy={p.y}\n                  r=\"4\"\n                  className=\"fill-primary\"\n                />\n              ))}\n            </svg>\n          </div>\n          {data.length > 0 && (\n            <div className=\"flex justify-between\">\n              {data.map((d, i) => (\n                <div\n                  key={i}\n                  className=\"text-xs text-muted-foreground text-center\"\n                  style={{ width: `${100 / data.length}%` }}\n                >\n                  {d.label}\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n      );\n    },\n\n    // ── Form Inputs ───────────────────────────────────────────────────\n\n    Input: ({ props, bindings, emit }) => {\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(\"\");\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      const hasValidation = !!(bindings?.value && props.checks?.length);\n      const { errors, validate } = useFieldValidation(\n        bindings?.value ?? \"\",\n        hasValidation ? { checks: props.checks ?? [] } : undefined,\n      );\n\n      return (\n        <div className=\"space-y-2\">\n          {props.label && <Label htmlFor={props.name}>{props.label}</Label>}\n          <Input\n            id={props.name}\n            name={props.name}\n            type={props.type ?? \"text\"}\n            placeholder={props.placeholder ?? \"\"}\n            value={value}\n            onChange={(e) => setValue(e.target.value)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\") emit(\"submit\");\n            }}\n            onFocus={() => emit(\"focus\")}\n            onBlur={() => {\n              if (hasValidation) validate();\n              emit(\"blur\");\n            }}\n          />\n          {errors.length > 0 && (\n            <p className=\"text-sm text-destructive\">{errors[0]}</p>\n          )}\n        </div>\n      );\n    },\n\n    Textarea: ({ props, bindings }) => {\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(\"\");\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      const hasValidation = !!(bindings?.value && props.checks?.length);\n      const { errors, validate } = useFieldValidation(\n        bindings?.value ?? \"\",\n        hasValidation ? { checks: props.checks ?? [] } : undefined,\n      );\n\n      return (\n        <div className=\"space-y-2\">\n          {props.label && <Label htmlFor={props.name}>{props.label}</Label>}\n          <Textarea\n            id={props.name}\n            name={props.name}\n            placeholder={props.placeholder ?? \"\"}\n            rows={props.rows ?? 3}\n            value={value}\n            onChange={(e) => setValue(e.target.value)}\n            onBlur={() => {\n              if (hasValidation) validate();\n            }}\n          />\n          {errors.length > 0 && (\n            <p className=\"text-sm text-destructive\">{errors[0]}</p>\n          )}\n        </div>\n      );\n    },\n\n    Select: ({ props, bindings, emit }) => {\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState<string>(\"\");\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n      const rawOptions = props.options ?? [];\n      // Coerce options to strings – AI may produce objects/numbers instead of\n      // plain strings which would cause duplicate `[object Object]` keys.\n      const options = rawOptions.map((opt) =>\n        typeof opt === \"string\" ? opt : String(opt ?? \"\"),\n      );\n\n      const hasValidation = !!(bindings?.value && props.checks?.length);\n      const { errors, validate } = useFieldValidation(\n        bindings?.value ?? \"\",\n        hasValidation ? { checks: props.checks ?? [] } : undefined,\n      );\n\n      return (\n        <div className=\"space-y-2\">\n          <Label>{props.label}</Label>\n          <Select\n            value={value}\n            onValueChange={(v) => {\n              setValue(v);\n              if (hasValidation) validate();\n              emit(\"change\");\n            }}\n          >\n            <SelectTrigger className=\"w-full\">\n              <SelectValue placeholder={props.placeholder ?? \"Select...\"} />\n            </SelectTrigger>\n            <SelectContent>\n              {options.map((opt, idx) => (\n                <SelectItem\n                  key={`${idx}-${opt}`}\n                  value={opt || `option-${idx}`}\n                >\n                  {opt}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n          {errors.length > 0 && (\n            <p className=\"text-sm text-destructive\">{errors[0]}</p>\n          )}\n        </div>\n      );\n    },\n\n    Checkbox: ({ props, bindings, emit }) => {\n      const [boundChecked, setBoundChecked] = useBoundProp<boolean>(\n        props.checked as boolean | undefined,\n        bindings?.checked,\n      );\n      const [localChecked, setLocalChecked] = useState(!!props.checked);\n      const isBound = !!bindings?.checked;\n      const checked = isBound ? (boundChecked ?? false) : localChecked;\n      const setChecked = isBound ? setBoundChecked : setLocalChecked;\n\n      return (\n        <div className=\"flex items-center space-x-2\">\n          <Checkbox\n            id={props.name}\n            checked={checked}\n            onCheckedChange={(c) => {\n              setChecked(c === true);\n              emit(\"change\");\n            }}\n          />\n          <Label htmlFor={props.name} className=\"cursor-pointer\">\n            {props.label}\n          </Label>\n        </div>\n      );\n    },\n\n    Radio: ({ props, bindings, emit }) => {\n      const rawOptions = props.options ?? [];\n      const options = rawOptions.map((opt) =>\n        typeof opt === \"string\" ? opt : String(opt ?? \"\"),\n      );\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(options[0] ?? \"\");\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      return (\n        <div className=\"space-y-2\">\n          {props.label && <Label>{props.label}</Label>}\n          <RadioGroup\n            value={value}\n            onValueChange={(v) => {\n              setValue(v);\n              emit(\"change\");\n            }}\n          >\n            {options.map((opt, idx) => (\n              <div\n                key={`${idx}-${opt}`}\n                className=\"flex items-center space-x-2\"\n              >\n                <RadioGroupItem\n                  value={opt || `option-${idx}`}\n                  id={`${props.name}-${idx}-${opt}`}\n                />\n                <Label\n                  htmlFor={`${props.name}-${idx}-${opt}`}\n                  className=\"cursor-pointer\"\n                >\n                  {opt}\n                </Label>\n              </div>\n            ))}\n          </RadioGroup>\n        </div>\n      );\n    },\n\n    Switch: ({ props, bindings, emit }) => {\n      const [boundChecked, setBoundChecked] = useBoundProp<boolean>(\n        props.checked as boolean | undefined,\n        bindings?.checked,\n      );\n      const [localChecked, setLocalChecked] = useState(!!props.checked);\n      const isBound = !!bindings?.checked;\n      const checked = isBound ? (boundChecked ?? false) : localChecked;\n      const setChecked = isBound ? setBoundChecked : setLocalChecked;\n\n      return (\n        <div className=\"flex items-center justify-between space-x-2\">\n          <Label htmlFor={props.name} className=\"cursor-pointer\">\n            {props.label}\n          </Label>\n          <Switch\n            id={props.name}\n            checked={checked}\n            onCheckedChange={(c) => {\n              setChecked(c);\n              emit(\"change\");\n            }}\n          />\n        </div>\n      );\n    },\n\n    Slider: ({ props, bindings, emit }) => {\n      const [boundValue, setBoundValue] = useBoundProp<number>(\n        props.value as number | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(props.min ?? 0);\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? props.min ?? 0) : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      return (\n        <div className=\"space-y-2\">\n          {props.label && (\n            <div className=\"flex justify-between\">\n              <Label className=\"text-sm\">{props.label}</Label>\n              <span className=\"text-sm text-muted-foreground\">{value}</span>\n            </div>\n          )}\n          <Slider\n            value={[value]}\n            min={props.min ?? 0}\n            max={props.max ?? 100}\n            step={props.step ?? 1}\n            onValueChange={(v) => {\n              setValue(v[0] ?? 0);\n              emit(\"change\");\n            }}\n          />\n        </div>\n      );\n    },\n\n    // ── Actions ───────────────────────────────────────────────────────\n\n    Button: ({ props, emit }) => {\n      const variant =\n        props.variant === \"danger\"\n          ? \"destructive\"\n          : props.variant === \"secondary\"\n            ? \"secondary\"\n            : \"default\";\n\n      return (\n        <Button\n          variant={variant}\n          disabled={props.disabled ?? false}\n          onClick={() => emit(\"press\")}\n        >\n          {props.label}\n        </Button>\n      );\n    },\n\n    Link: ({ props, emit }) => (\n      <Button\n        variant=\"link\"\n        className=\"h-auto p-0\"\n        onClick={() => emit(\"press\")}\n      >\n        {props.label}\n      </Button>\n    ),\n\n    DropdownMenu: ({ props, emit }) => {\n      const items = props.items ?? [];\n      return (\n        <DropdownMenuPrimitive>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\">{props.label}</Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent>\n            {items.map((item) => (\n              <DropdownMenuItem key={item.value} onClick={() => emit(\"select\")}>\n                {item.label}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenuPrimitive>\n      );\n    },\n\n    Toggle: ({ props, bindings, emit }) => {\n      const [boundPressed, setBoundPressed] = useBoundProp<boolean>(\n        props.pressed as boolean | undefined,\n        bindings?.pressed,\n      );\n      const [localPressed, setLocalPressed] = useState(props.pressed ?? false);\n      const isBound = !!bindings?.pressed;\n      const pressed = isBound ? (boundPressed ?? false) : localPressed;\n      const setPressed = isBound ? setBoundPressed : setLocalPressed;\n\n      return (\n        <Toggle\n          variant={props.variant ?? \"default\"}\n          pressed={pressed}\n          onPressedChange={(v) => {\n            setPressed(v);\n            emit(\"change\");\n          }}\n        >\n          {props.label}\n        </Toggle>\n      );\n    },\n\n    ToggleGroup: ({ props, bindings, emit }) => {\n      const type = props.type ?? \"single\";\n      const items = props.items ?? [];\n      const [boundValue, setBoundValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const [localValue, setLocalValue] = useState(items[0]?.value ?? \"\");\n      const isBound = !!bindings?.value;\n      const value = isBound ? (boundValue ?? \"\") : localValue;\n      const setValue = isBound ? setBoundValue : setLocalValue;\n\n      if (type === \"multiple\") {\n        return (\n          <ToggleGroup type=\"multiple\">\n            {items.map((item) => (\n              <ToggleGroupItem key={item.value} value={item.value}>\n                {item.label}\n              </ToggleGroupItem>\n            ))}\n          </ToggleGroup>\n        );\n      }\n\n      return (\n        <ToggleGroup\n          type=\"single\"\n          value={value}\n          onValueChange={(v) => {\n            if (v) {\n              setValue(v);\n              emit(\"change\");\n            }\n          }}\n        >\n          {items.map((item) => (\n            <ToggleGroupItem key={item.value} value={item.value}>\n              {item.label}\n            </ToggleGroupItem>\n          ))}\n        </ToggleGroup>\n      );\n    },\n\n    ButtonGroup: ({ props, bindings, emit }) => {\n      const buttons = props.buttons ?? [];\n      const [boundSelected, setBoundSelected] = useBoundProp<string>(\n        props.selected as string | undefined,\n        bindings?.selected,\n      );\n      const [localValue, setLocalValue] = useState(buttons[0]?.value ?? \"\");\n      const isBound = !!bindings?.selected;\n      const value = isBound ? (boundSelected ?? \"\") : localValue;\n      const setValue = isBound ? setBoundSelected : setLocalValue;\n\n      return (\n        <div className=\"inline-flex rounded-md border border-border\">\n          {buttons.map((btn, i) => (\n            <button\n              key={btn.value}\n              className={`px-3 py-1.5 text-sm transition-colors ${\n                value === btn.value\n                  ? \"bg-primary text-primary-foreground\"\n                  : \"bg-background hover:bg-muted\"\n              } ${i > 0 ? \"border-l border-border\" : \"\"} ${\n                i === 0 ? \"rounded-l-md\" : \"\"\n              } ${i === buttons.length - 1 ? \"rounded-r-md\" : \"\"}`}\n              onClick={() => {\n                setValue(btn.value);\n                emit(\"change\");\n              }}\n            >\n              {btn.label}\n            </button>\n          ))}\n        </div>\n      );\n    },\n\n    Pagination: ({ props, bindings, emit }) => {\n      const [boundPage, setBoundPage] = useBoundProp<number>(\n        props.page as number | undefined,\n        bindings?.page,\n      );\n      const currentPage = boundPage ?? 1;\n      const pages = Array.from({ length: props.totalPages }, (_, i) => i + 1);\n\n      return (\n        <PaginationPrimitive>\n          <PaginationContent>\n            <PaginationItem>\n              <PaginationPrevious\n                href=\"#\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  if (currentPage > 1) {\n                    setBoundPage(currentPage - 1);\n                    emit(\"change\");\n                  }\n                }}\n              />\n            </PaginationItem>\n            {pages.map((page) => (\n              <PaginationItem key={page}>\n                <PaginationLink\n                  href=\"#\"\n                  isActive={page === currentPage}\n                  onClick={(e) => {\n                    e.preventDefault();\n                    setBoundPage(page);\n                    emit(\"change\");\n                  }}\n                >\n                  {page}\n                </PaginationLink>\n              </PaginationItem>\n            ))}\n            <PaginationItem>\n              <PaginationNext\n                href=\"#\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  if (currentPage < props.totalPages) {\n                    setBoundPage(currentPage + 1);\n                    emit(\"change\");\n                  }\n                }}\n              />\n            </PaginationItem>\n          </PaginationContent>\n        </PaginationPrimitive>\n      );\n    },\n  },\n\n  actions: {\n    // Built-in state actions — handled by ActionProvider, stubs needed for types\n    setState: async () => {},\n    pushState: async () => {},\n    removeState: async () => {},\n\n    // Demo actions — show toasts\n    buttonClick: async (params) => {\n      const message = (params?.message as string) || \"Button clicked!\";\n      toast.success(message);\n    },\n\n    formSubmit: async (params) => {\n      const formName = (params?.formName as string) || \"Form\";\n      toast.success(`${formName} submitted successfully!`);\n    },\n\n    linkClick: async (params) => {\n      const href = (params?.href as string) || \"#\";\n      toast.info(`Navigating to: ${href}`);\n    },\n  },\n});\n\n// Fallback component for unknown types\nexport function Fallback({ type }: { type: string }) {\n  return <div className=\"text-xs text-muted-foreground\">[{type}]</div>;\n}\n"
  },
  {
    "path": "apps/web/lib/render/renderer.tsx",
    "content": "\"use client\";\n\nimport { useRef, useMemo, type ReactNode } from \"react\";\nimport { toast } from \"sonner\";\nimport {\n  Renderer,\n  type Spec,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n  ValidationProvider,\n  useValidation,\n} from \"@json-render/react\";\n\nimport { registry, Fallback } from \"./registry\";\n\n// =============================================================================\n// PlaygroundRenderer\n// =============================================================================\n\ninterface PlaygroundRendererProps {\n  spec: Spec | null;\n  data?: Record<string, unknown>;\n  loading?: boolean;\n}\n\nconst fallbackRenderer = (renderProps: { element: { type: string } }) => (\n  <Fallback type={renderProps.element.type} />\n);\n\n/**\n * Inner component that sits inside ValidationProvider so it can call\n * useValidation() and wire validateAll into the formSubmit action handler.\n *\n * ActionProvider stores `handlers` in useState, so it only reads the initial\n * value. We use a ref so the handlers object is stable (created once) but\n * formSubmit always reads the latest validateAll.\n */\nfunction ValidatedActions({ children }: { children: ReactNode }) {\n  const { validateAll } = useValidation();\n  const validateAllRef = useRef(validateAll);\n  validateAllRef.current = validateAll;\n\n  const handlers = useMemo<\n    Record<string, (params: Record<string, unknown>) => void>\n  >(\n    () => ({\n      buttonClick: (params) => {\n        const message = (params?.message as string) || \"Button clicked!\";\n        toast.success(message);\n      },\n      formSubmit: () => {\n        const allValid = validateAllRef.current();\n        if (!allValid) {\n          toast.error(\"Please fix the errors before submitting.\");\n          return;\n        }\n        toast.success(\"Form submitted successfully!\");\n      },\n      linkClick: (params) => {\n        const href = (params?.href as string) || \"#\";\n        toast.info(`Navigating to: ${href}`);\n      },\n    }),\n    [], // stable — ref ensures latest validateAll is always used\n  );\n\n  return <ActionProvider handlers={handlers}>{children}</ActionProvider>;\n}\n\nexport function PlaygroundRenderer({\n  spec,\n  data,\n  loading,\n}: PlaygroundRendererProps): ReactNode {\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={data ?? spec.state}>\n      <VisibilityProvider>\n        <ValidationProvider>\n          <ValidatedActions>\n            <Renderer\n              spec={spec}\n              registry={registry}\n              fallback={fallbackRenderer}\n              loading={loading}\n            />\n          </ValidatedActions>\n        </ValidationProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/lib/search-index.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { docsNavigation } from \"./docs-navigation\";\nimport { mdxToCleanMarkdown } from \"./mdx-to-markdown\";\n\nexport type IndexEntry = {\n  title: string;\n  href: string;\n  section: string;\n  content: string;\n};\n\nlet cached: IndexEntry[] | null = null;\n\nfunction stripMarkdown(md: string): string {\n  return (\n    md\n      // Remove fenced code blocks entirely\n      .replace(/```[\\s\\S]*?```/g, \"\")\n      // Remove inline code\n      .replace(/`[^`]+`/g, \"\")\n      // Remove markdown links, keep text\n      .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n      // Remove heading markers\n      .replace(/^#{1,6}\\s+/gm, \"\")\n      // Remove bold/italic markers\n      .replace(/\\*{1,3}([^*]+)\\*{1,3}/g, \"$1\")\n      // Remove HTML tags\n      .replace(/<[^>]+>/g, \"\")\n      // Collapse whitespace\n      .replace(/\\n{3,}/g, \"\\n\\n\")\n      .trim()\n  );\n}\n\nfunction mdxFileForSlug(slug: string): string {\n  const docsRoot = join(process.cwd(), \"app\", \"(main)\", \"docs\");\n  if (slug === \"/docs\") {\n    return join(docsRoot, \"page.mdx\");\n  }\n  const rest = slug.replace(/^\\/docs\\/?/, \"\");\n  return join(docsRoot, ...rest.split(\"/\"), \"page.mdx\");\n}\n\nexport async function getSearchIndex(): Promise<IndexEntry[]> {\n  if (cached) return cached;\n\n  const entries: IndexEntry[] = [];\n\n  for (const section of docsNavigation) {\n    for (const item of section.items) {\n      if (item.external) continue;\n      try {\n        const raw = await readFile(mdxFileForSlug(item.href), \"utf-8\");\n        const md = mdxToCleanMarkdown(raw);\n        const content = stripMarkdown(md);\n        entries.push({\n          title: item.title,\n          href: item.href,\n          section: section.title,\n          content,\n        });\n      } catch {\n        entries.push({\n          title: item.title,\n          href: item.href,\n          section: section.title,\n          content: \"\",\n        });\n      }\n    }\n  }\n\n  cached = entries;\n  return entries;\n}\n"
  },
  {
    "path": "apps/web/lib/spec-patch.ts",
    "content": "import type { Spec, JsonPatch } from \"@json-render/core\";\nimport { setByPath, getByPath, removeByPath } from \"@json-render/core\";\n\nexport function setSpecValue(\n  newSpec: Spec,\n  path: string,\n  value: unknown,\n): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    setByPath(\n      newSpec.state as Record<string, unknown>,\n      path.slice(\"/state\".length),\n      value,\n    );\n    return;\n  }\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n    if (pathParts.length === 1) {\n      if (value == null || typeof value !== \"object\") return;\n      const el = value as Record<string, unknown>;\n      newSpec.elements[elementKey] = {\n        ...el,\n        type: typeof el.type === \"string\" ? el.type : \"\",\n        props: el.props != null && typeof el.props === \"object\" ? el.props : {},\n        children: Array.isArray(el.children) ? el.children : [],\n      } as Spec[\"elements\"][string];\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          \"/\" + pathParts.slice(1).join(\"/\"),\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\nexport function removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    delete newSpec.state;\n    return;\n  }\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    removeByPath(\n      newSpec.state as Record<string, unknown>,\n      path.slice(\"/state\".length),\n    );\n    return;\n  }\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n    if (pathParts.length === 1) {\n      delete newSpec.elements[elementKey];\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          \"/\" + pathParts.slice(1).join(\"/\"),\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\nexport function getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  if (path === \"/state\") return spec.state;\n  if (path.startsWith(\"/state/\") && spec.state) {\n    return getByPath(\n      spec.state as Record<string, unknown>,\n      path.slice(\"/state\".length),\n    );\n  }\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\nexport function normalizeSpec(spec: Spec): void {\n  if (\n    spec.state === null ||\n    (spec.state !== undefined && typeof spec.state !== \"object\")\n  ) {\n    spec.state = undefined;\n  }\n\n  for (const key of Object.keys(spec.elements)) {\n    const el = spec.elements[key];\n    if (!el || typeof el !== \"object\") {\n      delete spec.elements[key];\n      continue;\n    }\n    if (el.props == null || typeof el.props !== \"object\") {\n      spec.elements[key] = { ...el, props: {} } as Spec[\"elements\"][string];\n    }\n    if (!Array.isArray(spec.elements[key]!.children)) {\n      spec.elements[key] = {\n        ...spec.elements[key]!,\n        children: [],\n      } as Spec[\"elements\"][string];\n    }\n  }\n}\n\nexport function applySpecPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\":\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    case \"remove\":\n      removeSpecValue(newSpec, patch.path);\n      break;\n    case \"move\":\n      if (patch.from) {\n        const moveValue = getSpecValue(newSpec, patch.from);\n        removeSpecValue(newSpec, patch.from);\n        setSpecValue(newSpec, patch.path, moveValue);\n      }\n      break;\n    case \"copy\":\n      if (patch.from) {\n        setSpecValue(newSpec, patch.path, getSpecValue(newSpec, patch.from));\n      }\n      break;\n    case \"test\":\n      break;\n  }\n  normalizeSpec(newSpec);\n  return newSpec;\n}\n"
  },
  {
    "path": "apps/web/lib/use-playground-stream.ts",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport type { Spec, JsonPatch, EditMode } from \"@json-render/core\";\nimport { deepMergeSpec, diffToPatches } from \"@json-render/core\";\nimport { parse as yamlParse, stringify as yamlStringify } from \"yaml\";\nimport { applyPatch as applyUnifiedDiff } from \"diff\";\nimport {\n  createYamlStreamCompiler,\n  YAML_SPEC_FENCE,\n  YAML_EDIT_FENCE,\n  YAML_PATCH_FENCE,\n  DIFF_FENCE,\n  FENCE_CLOSE,\n} from \"@json-render/yaml\";\nimport { applySpecPatch } from \"./spec-patch\";\n\nexport type StreamFormat = \"jsonl\" | \"yaml\";\n\nexport interface TokenUsage {\n  promptTokens: number;\n  completionTokens: number;\n  totalTokens: number;\n  cachedTokens: number;\n  cacheWriteTokens: number;\n}\n\nexport interface UsePlaygroundStreamOptions {\n  api: string;\n  format: StreamFormat;\n  editModes?: EditMode[];\n  onError?: (error: Error) => void;\n  onComplete?: (spec: Spec) => void;\n}\n\nexport interface UsePlaygroundStreamReturn {\n  spec: Spec | null;\n  isStreaming: boolean;\n  error: Error | null;\n  usage: TokenUsage | null;\n  rawLines: string[];\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  clear: () => void;\n}\n\n// ── JSONL helpers ──\n\ntype ParsedLine =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"usage\"; usage: TokenUsage }\n  | { type: \"json-edit\"; mergeObj: Record<string, unknown> }\n  | null;\n\nfunction parseLine(line: string): ParsedLine {\n  try {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"//\")) return null;\n    const parsed = JSON.parse(trimmed);\n    if (parsed.__meta === \"usage\") {\n      return {\n        type: \"usage\",\n        usage: {\n          promptTokens: parsed.promptTokens ?? 0,\n          completionTokens: parsed.completionTokens ?? 0,\n          totalTokens: parsed.totalTokens ?? 0,\n          cachedTokens: parsed.cachedTokens ?? 0,\n          cacheWriteTokens: parsed.cacheWriteTokens ?? 0,\n        },\n      };\n    }\n    if (parsed.__json_edit === true) {\n      const mergeObj = { ...parsed };\n      delete mergeObj.__json_edit;\n      return { type: \"json-edit\", mergeObj };\n    }\n    return { type: \"patch\", patch: parsed as JsonPatch };\n  } catch {\n    return null;\n  }\n}\n\ntype FenceState = \"outside\" | \"yaml-spec\" | \"yaml-edit\" | \"yaml-patch\" | \"diff\";\n\n// ── Hook ──\n\nexport function usePlaygroundStream({\n  api,\n  format,\n  editModes,\n  onError,\n  onComplete,\n}: UsePlaygroundStreamOptions): UsePlaygroundStreamReturn {\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const [usage, setUsage] = useState<TokenUsage | null>(null);\n  const [rawLines, setRawLines] = useState<string[]>([]);\n  const rawLinesRef = useRef<string[]>([]);\n  const abortControllerRef = useRef<AbortController | null>(null);\n\n  const onCompleteRef = useRef(onComplete);\n  onCompleteRef.current = onComplete;\n  const onErrorRef = useRef(onError);\n  onErrorRef.current = onError;\n  const formatRef = useRef(format);\n  formatRef.current = format;\n  const editModesRef = useRef(editModes);\n  editModesRef.current = editModes;\n\n  const clear = useCallback(() => {\n    setSpec(null);\n    setError(null);\n  }, []);\n\n  const send = useCallback(\n    async (prompt: string, context?: Record<string, unknown>) => {\n      abortControllerRef.current = new AbortController();\n\n      setIsStreaming(true);\n      setError(null);\n      setUsage(null);\n      rawLinesRef.current = [];\n      setRawLines([]);\n\n      const previousSpec = context?.previousSpec as Spec | undefined;\n      let currentSpec: Spec =\n        previousSpec && previousSpec.root\n          ? { ...previousSpec, elements: { ...previousSpec.elements } }\n          : { root: \"\", elements: {} };\n      setSpec(currentSpec);\n\n      try {\n        const response = await fetch(api, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            prompt,\n            context,\n            format: formatRef.current,\n            editModes: editModesRef.current,\n          }),\n          signal: abortControllerRef.current.signal,\n        });\n\n        if (!response.ok) {\n          let errorMessage = `HTTP error: ${response.status}`;\n          try {\n            const errorData = await response.json();\n            if (errorData.message) errorMessage = errorData.message;\n            else if (errorData.error) errorMessage = errorData.error;\n          } catch {\n            // use default\n          }\n          throw new Error(errorMessage);\n        }\n\n        const reader = response.body?.getReader();\n        if (!reader) throw new Error(\"No response body\");\n\n        const decoder = new TextDecoder();\n        let buffer = \"\";\n\n        if (formatRef.current === \"yaml\") {\n          // ── YAML streaming ──\n          let fenceState: FenceState = \"outside\";\n          const compiler = createYamlStreamCompiler<Record<string, unknown>>();\n          let yamlEditAccumulated = \"\";\n          let yamlPatchAccumulated = \"\";\n          let diffAccumulated = \"\";\n\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split(\"\\n\");\n            buffer = lines.pop() ?? \"\";\n\n            for (const line of lines) {\n              const trimmed = line.trim();\n\n              // Check for usage metadata (appended after stream)\n              if (trimmed.startsWith(\"{\") && trimmed.includes('\"__meta\"')) {\n                try {\n                  const parsed = JSON.parse(trimmed);\n                  if (parsed.__meta === \"usage\") {\n                    setUsage({\n                      promptTokens: parsed.promptTokens ?? 0,\n                      completionTokens: parsed.completionTokens ?? 0,\n                      totalTokens: parsed.totalTokens ?? 0,\n                      cachedTokens: parsed.cachedTokens ?? 0,\n                      cacheWriteTokens: parsed.cacheWriteTokens ?? 0,\n                    });\n                    continue;\n                  }\n                } catch {\n                  // not JSON\n                }\n              }\n\n              rawLinesRef.current.push(line);\n\n              if (fenceState === \"outside\") {\n                if (\n                  trimmed === YAML_SPEC_FENCE ||\n                  trimmed.startsWith(YAML_SPEC_FENCE + \" \")\n                ) {\n                  fenceState = \"yaml-spec\";\n                  compiler.reset();\n                } else if (\n                  trimmed === YAML_EDIT_FENCE ||\n                  trimmed.startsWith(YAML_EDIT_FENCE + \" \")\n                ) {\n                  fenceState = \"yaml-edit\";\n                  yamlEditAccumulated = \"\";\n                } else if (\n                  trimmed === YAML_PATCH_FENCE ||\n                  trimmed.startsWith(YAML_PATCH_FENCE + \" \")\n                ) {\n                  fenceState = \"yaml-patch\";\n                  yamlPatchAccumulated = \"\";\n                } else if (\n                  trimmed === DIFF_FENCE ||\n                  trimmed.startsWith(DIFF_FENCE + \" \")\n                ) {\n                  fenceState = \"diff\";\n                  diffAccumulated = \"\";\n                }\n              } else if (trimmed === FENCE_CLOSE || trimmed === \"````\") {\n                if (fenceState === \"yaml-spec\") {\n                  const { result, newPatches } = compiler.flush();\n                  if (result && typeof result === \"object\" && result.root) {\n                    for (const patch of newPatches) {\n                      currentSpec = applySpecPatch(currentSpec, patch);\n                    }\n                    setSpec({ ...currentSpec });\n                  }\n                } else if (fenceState === \"yaml-edit\") {\n                  try {\n                    const editObj = yamlParse(yamlEditAccumulated);\n                    if (editObj && typeof editObj === \"object\") {\n                      const merged = deepMergeSpec(\n                        currentSpec as unknown as Record<string, unknown>,\n                        editObj as Record<string, unknown>,\n                      );\n                      const patches = diffToPatches(\n                        currentSpec as unknown as Record<string, unknown>,\n                        merged,\n                      );\n                      for (const patch of patches) {\n                        currentSpec = applySpecPatch(currentSpec, patch);\n                      }\n                      setSpec({ ...currentSpec });\n                    }\n                  } catch {\n                    // Invalid YAML edit\n                  }\n                } else if (fenceState === \"yaml-patch\") {\n                  for (const patchLine of yamlPatchAccumulated.split(\"\\n\")) {\n                    const t = patchLine.trim();\n                    if (!t) continue;\n                    try {\n                      const patch = JSON.parse(t) as JsonPatch;\n                      if (patch.op) {\n                        currentSpec = applySpecPatch(currentSpec, patch);\n                      }\n                    } catch {\n                      // Skip invalid JSON lines\n                    }\n                  }\n                  setSpec({ ...currentSpec });\n                } else if (fenceState === \"diff\") {\n                  try {\n                    const specYaml = yamlStringify(currentSpec, { indent: 2 });\n                    const patched = applyUnifiedDiff(specYaml, diffAccumulated);\n                    if (typeof patched === \"string\") {\n                      const parsed = yamlParse(patched);\n                      if (parsed && typeof parsed === \"object\") {\n                        const patches = diffToPatches(\n                          currentSpec as unknown as Record<string, unknown>,\n                          parsed as Record<string, unknown>,\n                        );\n                        for (const patch of patches) {\n                          currentSpec = applySpecPatch(currentSpec, patch);\n                        }\n                        setSpec({ ...currentSpec });\n                      }\n                    }\n                  } catch {\n                    // Diff apply or reparse failed\n                  }\n                }\n                fenceState = \"outside\";\n              } else if (fenceState === \"yaml-spec\") {\n                const { newPatches } = compiler.push(line + \"\\n\");\n                if (newPatches.length > 0) {\n                  for (const patch of newPatches) {\n                    currentSpec = applySpecPatch(currentSpec, patch);\n                  }\n                  setSpec({ ...currentSpec });\n                }\n              } else if (fenceState === \"yaml-edit\") {\n                yamlEditAccumulated += line + \"\\n\";\n              } else if (fenceState === \"yaml-patch\") {\n                yamlPatchAccumulated += line + \"\\n\";\n              } else if (fenceState === \"diff\") {\n                diffAccumulated += line + \"\\n\";\n              }\n            }\n            setRawLines([...rawLinesRef.current]);\n          }\n\n          // Process remaining buffer\n          if (buffer.trim()) {\n            const trimmed = buffer.trim();\n            if (trimmed.startsWith(\"{\") && trimmed.includes('\"__meta\"')) {\n              try {\n                const parsed = JSON.parse(trimmed);\n                if (parsed.__meta === \"usage\") {\n                  setUsage({\n                    promptTokens: parsed.promptTokens ?? 0,\n                    completionTokens: parsed.completionTokens ?? 0,\n                    totalTokens: parsed.totalTokens ?? 0,\n                    cachedTokens: parsed.cachedTokens ?? 0,\n                    cacheWriteTokens: parsed.cacheWriteTokens ?? 0,\n                  });\n                }\n              } catch {\n                // not JSON\n              }\n            } else if (fenceState === \"yaml-spec\") {\n              compiler.push(buffer);\n              const { result, newPatches } = compiler.flush();\n              if (result && typeof result === \"object\" && result.root) {\n                for (const patch of newPatches) {\n                  currentSpec = applySpecPatch(currentSpec, patch);\n                }\n                setSpec({ ...currentSpec });\n              }\n            }\n          }\n        } else {\n          // ── JSONL streaming ──\n          let jsonlDiffState: \"outside\" | \"diff\" = \"outside\";\n          let jsonlDiffAccumulated = \"\";\n\n          while (true) {\n            const { done, value } = await reader.read();\n            if (done) break;\n\n            buffer += decoder.decode(value, { stream: true });\n            const lines = buffer.split(\"\\n\");\n            buffer = lines.pop() ?? \"\";\n\n            for (const line of lines) {\n              const trimmed = line.trim();\n              if (!trimmed) continue;\n\n              // Diff fence detection within JSONL mode\n              if (jsonlDiffState === \"outside\") {\n                if (\n                  trimmed === DIFF_FENCE ||\n                  trimmed.startsWith(DIFF_FENCE + \" \")\n                ) {\n                  jsonlDiffState = \"diff\";\n                  jsonlDiffAccumulated = \"\";\n                  continue;\n                }\n              } else if (\n                jsonlDiffState === \"diff\" &&\n                (trimmed === FENCE_CLOSE || trimmed === \"````\")\n              ) {\n                try {\n                  const specJson = JSON.stringify(currentSpec, null, 2);\n                  const patched = applyUnifiedDiff(\n                    specJson,\n                    jsonlDiffAccumulated,\n                  );\n                  if (typeof patched === \"string\") {\n                    const parsed = JSON.parse(patched);\n                    if (parsed && typeof parsed === \"object\") {\n                      const patches = diffToPatches(\n                        currentSpec as unknown as Record<string, unknown>,\n                        parsed as Record<string, unknown>,\n                      );\n                      for (const patch of patches) {\n                        currentSpec = applySpecPatch(currentSpec, patch);\n                      }\n                      setSpec({ ...currentSpec });\n                    }\n                  }\n                } catch {\n                  // Diff apply failed\n                }\n                jsonlDiffState = \"outside\";\n                continue;\n              }\n\n              if (jsonlDiffState === \"diff\") {\n                jsonlDiffAccumulated += line + \"\\n\";\n                rawLinesRef.current.push(line);\n                continue;\n              }\n\n              // Standard JSONL line parsing\n              const result = parseLine(trimmed);\n              if (!result) continue;\n              if (result.type === \"usage\") {\n                setUsage(result.usage);\n              } else if (result.type === \"json-edit\") {\n                const merged = deepMergeSpec(\n                  currentSpec as unknown as Record<string, unknown>,\n                  result.mergeObj,\n                );\n                const patches = diffToPatches(\n                  currentSpec as unknown as Record<string, unknown>,\n                  merged,\n                );\n                for (const patch of patches) {\n                  currentSpec = applySpecPatch(currentSpec, patch);\n                }\n                rawLinesRef.current.push(trimmed);\n                setSpec({ ...currentSpec });\n              } else {\n                rawLinesRef.current.push(trimmed);\n                currentSpec = applySpecPatch(currentSpec, result.patch);\n                setSpec({ ...currentSpec });\n              }\n            }\n            setRawLines([...rawLinesRef.current]);\n          }\n\n          if (buffer.trim()) {\n            const trimmed = buffer.trim();\n            const result = parseLine(trimmed);\n            if (result) {\n              if (result.type === \"usage\") {\n                setUsage(result.usage);\n              } else if (result.type === \"json-edit\") {\n                const merged = deepMergeSpec(\n                  currentSpec as unknown as Record<string, unknown>,\n                  result.mergeObj,\n                );\n                const patches = diffToPatches(\n                  currentSpec as unknown as Record<string, unknown>,\n                  merged,\n                );\n                for (const patch of patches) {\n                  currentSpec = applySpecPatch(currentSpec, patch);\n                }\n                rawLinesRef.current.push(trimmed);\n                setSpec({ ...currentSpec });\n              } else {\n                rawLinesRef.current.push(trimmed);\n                currentSpec = applySpecPatch(currentSpec, result.patch);\n                setSpec({ ...currentSpec });\n              }\n            }\n          }\n        }\n\n        onCompleteRef.current?.(currentSpec);\n      } catch (err) {\n        if ((err as Error).name === \"AbortError\") return;\n        const error = err instanceof Error ? err : new Error(String(err));\n        setError(error);\n        onErrorRef.current?.(error);\n      } finally {\n        setIsStreaming(false);\n      }\n    },\n    [api],\n  );\n\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  return { spec, isStreaming, error, usage, rawLines, send, clear };\n}\n"
  },
  {
    "path": "apps/web/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/web/mdx-components.tsx",
    "content": "import type { MDXComponents } from \"mdx/types\";\nimport Link from \"next/link\";\nimport { Code } from \"@/components/code\";\nimport { GenerationModesDiagram } from \"@/components/generation-modes-diagram\";\nimport { PackageInstall } from \"@/components/package-install\";\n\nfunction slugify(text: string): string {\n  return text\n    .toLowerCase()\n    .replace(/[^\\w\\s-]/g, \"\")\n    .replace(/\\s+/g, \"-\")\n    .trim();\n}\n\nfunction extractText(children: React.ReactNode): string {\n  if (typeof children === \"string\") return children;\n  if (typeof children === \"number\") return String(children);\n  if (Array.isArray(children)) return children.map(extractText).join(\"\");\n  if (children && typeof children === \"object\") {\n    const obj = children as unknown as Record<string, unknown>;\n    if (\"props\" in obj) {\n      const props = obj.props as { children?: React.ReactNode } | undefined;\n      return extractText(props?.children);\n    }\n  }\n  return \"\";\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n  return {\n    ...components,\n    h1: ({ children }: { children?: React.ReactNode }) => (\n      <h1 className=\"text-3xl font-bold mb-4\">{children}</h1>\n    ),\n    h2: ({ children }: { children?: React.ReactNode }) => {\n      const id = slugify(extractText(children));\n      return (\n        <h2 id={id} className=\"group text-xl font-semibold mt-12 mb-4\">\n          <a href={`#${id}`} className=\"no-underline hover:no-underline\">\n            {children}\n            <span className=\"ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none\">\n              #\n            </span>\n          </a>\n        </h2>\n      );\n    },\n    h3: ({ children }: { children?: React.ReactNode }) => {\n      const id = slugify(extractText(children));\n      return (\n        <h3 id={id} className=\"group text-lg font-medium mt-8 mb-3\">\n          <a href={`#${id}`} className=\"no-underline hover:no-underline\">\n            {children}\n            <span className=\"ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none\">\n              #\n            </span>\n          </a>\n        </h3>\n      );\n    },\n    p: ({ children }: { children?: React.ReactNode }) => (\n      <p className=\"text-sm text-muted-foreground mb-4 leading-relaxed\">\n        {children}\n      </p>\n    ),\n    ul: ({ children }: { children?: React.ReactNode }) => (\n      <ul className=\"list-disc list-inside text-sm text-muted-foreground space-y-1 mb-4\">\n        {children}\n      </ul>\n    ),\n    ol: ({ children }: { children?: React.ReactNode }) => (\n      <ol className=\"list-decimal list-inside space-y-2 text-sm text-muted-foreground mb-4\">\n        {children}\n      </ol>\n    ),\n    li: ({ children }: { children?: React.ReactNode }) => <li>{children}</li>,\n    a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {\n      if (href?.startsWith(\"/\")) {\n        return (\n          <Link href={href} className=\"text-foreground hover:underline\">\n            {children}\n          </Link>\n        );\n      }\n      return (\n        <a\n          href={href}\n          className=\"text-foreground hover:underline\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          {children}\n        </a>\n      );\n    },\n    code: ({\n      children,\n      className,\n    }: {\n      children?: React.ReactNode;\n      className?: string;\n    }) => {\n      // Fenced code blocks come through as <pre><code className=\"language-xxx\">\n      // Inline code has no className\n      if (className) {\n        // This is a fenced code block inside <pre> - handled by the pre component\n        return <code className={className}>{children}</code>;\n      }\n      return (\n        <code className=\"text-foreground bg-muted px-1.5 py-0.5 rounded text-sm\">\n          {children}\n        </code>\n      );\n    },\n    pre: async ({ children }: { children?: React.ReactNode }) => {\n      // Extract lang and code from the <code> child\n      const codeElement = children as React.ReactElement<{\n        className?: string;\n        children?: string;\n      }>;\n      const className = codeElement?.props?.className || \"\";\n      const lang = className.replace(\"language-\", \"\") || \"typescript\";\n      const code = codeElement?.props?.children || \"\";\n\n      return (\n        <Code\n          lang={lang as \"json\" | \"tsx\" | \"typescript\" | \"bash\" | \"javascript\"}\n        >\n          {typeof code === \"string\" ? code : String(code)}\n        </Code>\n      );\n    },\n    strong: ({ children }: { children?: React.ReactNode }) => (\n      <strong className=\"text-foreground font-medium\">{children}</strong>\n    ),\n    hr: () => <hr className=\"my-8 border-border\" />,\n    blockquote: ({ children }: { children?: React.ReactNode }) => (\n      <blockquote className=\"border-l-2 border-border pl-4 my-4 text-sm text-muted-foreground italic\">\n        {children}\n      </blockquote>\n    ),\n    table: ({ children }: { children?: React.ReactNode }) => (\n      <div className=\"my-6 overflow-x-auto\">\n        <table className=\"mdx-table w-full text-sm border-collapse\">\n          {children}\n        </table>\n      </div>\n    ),\n    th: ({ children }: { children?: React.ReactNode }) => (\n      <th className=\"border border-border px-4 py-2 text-left font-semibold bg-muted\">\n        {children}\n      </th>\n    ),\n    td: ({ children }: { children?: React.ReactNode }) => (\n      <td className=\"border border-border px-4 py-2 text-muted-foreground\">\n        {children}\n      </td>\n    ),\n    em: ({ children }: { children?: React.ReactNode }) => <em>{children}</em>,\n    // Custom components available in all MDX files\n    GenerationModesDiagram,\n    PackageInstall,\n  };\n}\n"
  },
  {
    "path": "apps/web/next.config.js",
    "content": "import createMDX from \"@next/mdx\";\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  serverExternalPackages: [\"bash-tool\", \"just-bash\", \"@mongodb-js/zstd\"],\n  pageExtensions: [\"js\", \"jsx\", \"ts\", \"tsx\", \"md\", \"mdx\"],\n  async redirects() {\n    return [\n      {\n        source: \"/docs/components\",\n        destination: \"/docs/registry\",\n        permanent: true,\n      },\n      {\n        source: \"/docs/actions\",\n        destination: \"/docs/registry#action-handlers\",\n        permanent: true,\n      },\n    ];\n  },\n};\n\nconst withMDX = createMDX({});\n\n/** @type {import('next').NextConfig} */\nconst config = withMDX(nextConfig);\nexport default config;\n"
  },
  {
    "path": "apps/web/package.json",
    "content": "{\n  \"name\": \"web\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint --max-warnings 0\",\n    \"check-types\": \"next typegen && tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.13\",\n    \"@ai-sdk/react\": \"3.0.79\",\n    \"@json-render/codegen\": \"workspace:*\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/yaml\": \"workspace:*\",\n    \"@mdx-js/loader\": \"^3.1.1\",\n    \"@mdx-js/mdx\": \"^3.1.1\",\n    \"@mdx-js/react\": \"^3.1.1\",\n    \"@next/mdx\": \"^16.1.6\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-tabs\": \"^1.1.13\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.36.1\",\n    \"@vercel/analytics\": \"^1.6.1\",\n    \"@vercel/speed-insights\": \"^1.3.1\",\n    \"@visual-json/react\": \"0.1.1\",\n    \"ai\": \"^6.0.33\",\n    \"bash-tool\": \"1.3.14\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"diff\": \"^8.0.3\",\n    \"embla-carousel-react\": \"^8.6.0\",\n    \"geist\": \"1.7.0\",\n    \"lucide-react\": \"^0.562.0\",\n    \"next\": \"16.1.1\",\n    \"next-themes\": \"^0.4.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\",\n    \"react-resizable-panels\": \"^4.4.1\",\n    \"remark-gfm\": \"4.0.1\",\n    \"shiki\": \"^3.21.0\",\n    \"sonner\": \"^2.0.7\",\n    \"streamdown\": \"2.1.0\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"unist-util-visit\": \"5.1.0\",\n    \"vaul\": \"^1.1.2\",\n    \"yaml\": \"^2.8.2\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"^22.15.3\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"eslint\": \"^9.39.1\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"5.9.2\"\n  }\n}\n"
  },
  {
    "path": "apps/web/postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"next-env.d.ts\",\n    \"next.config.js\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "examples/chat/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/chat/CHANGELOG.md",
    "content": "# example-chat\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/shadcn@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n  - @json-render/shadcn@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n  - @json-render/shadcn@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n  - @json-render/shadcn@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react@0.12.0\n  - @json-render/shadcn@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n  - @json-render/shadcn@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n  - @json-render/shadcn@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/shadcn@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n  - @json-render/shadcn@0.9.0\n"
  },
  {
    "path": "examples/chat/app/api/generate/route.ts",
    "content": "import { agent } from \"@/lib/agent\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport {\n  convertToModelMessages,\n  createUIMessageStream,\n  createUIMessageStreamResponse,\n  type UIMessage,\n} from \"ai\";\nimport { pipeJsonRender } from \"@json-render/core\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 60;\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const body = await req.json();\n  const uiMessages: UIMessage[] = body.messages;\n\n  if (!uiMessages || !Array.isArray(uiMessages) || uiMessages.length === 0) {\n    return new Response(\n      JSON.stringify({ error: \"messages array is required\" }),\n      {\n        status: 400,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const modelMessages = await convertToModelMessages(uiMessages);\n  const result = await agent.stream({ messages: modelMessages });\n\n  const stream = createUIMessageStream({\n    execute: async ({ writer }) => {\n      writer.merge(pipeJsonRender(result.toUIMessageStream()));\n    },\n  });\n\n  return createUIMessageStreamResponse({ stream });\n}\n"
  },
  {
    "path": "examples/chat/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@source \"../../../node_modules/streamdown/dist/*.js\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --radius-3xl: calc(var(--radius) + 12px);\n  --radius-4xl: calc(var(--radius) + 16px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbutton {\n  cursor: pointer;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -200% 0;\n  }\n  100% {\n    background-position: 200% 0;\n  }\n}\n\n.animate-shimmer {\n  background: linear-gradient(\n    90deg,\n    currentColor 25%,\n    hsl(0 0% 64%) 50%,\n    currentColor 75%\n  );\n  background-size: 200% 100%;\n  -webkit-background-clip: text;\n  background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer 2s ease-in-out infinite;\n}\n"
  },
  {
    "path": "examples/chat/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport { Toaster } from \"sonner\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport \"streamdown/styles.css\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"json-render Chat Example\",\n  description: \"AI-powered data explorer using ToolLoopAgent and json-render\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}\n      >\n        <ThemeProvider>\n          {children}\n          <Toaster />\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/chat/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport {\n  SPEC_DATA_PART,\n  SPEC_DATA_PART_TYPE,\n  type SpecDataPart,\n} from \"@json-render/core\";\nimport { useJsonRenderMessage } from \"@json-render/react\";\nimport { ExplorerRenderer } from \"@/lib/render/renderer\";\nimport { ThemeToggle } from \"@/components/theme-toggle\";\nimport {\n  ArrowDown,\n  ArrowUp,\n  ChevronRight,\n  Loader2,\n  Sparkles,\n} from \"lucide-react\";\nimport { Streamdown } from \"streamdown\";\nimport { code } from \"@streamdown/code\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype AppDataParts = { [SPEC_DATA_PART]: SpecDataPart };\ntype AppMessage = UIMessage<unknown, AppDataParts>;\n\n// =============================================================================\n// Transport\n// =============================================================================\n\nconst transport = new DefaultChatTransport({ api: \"/api/generate\" });\n\n// =============================================================================\n// Suggestions (shown in empty state)\n// =============================================================================\n\nconst SUGGESTIONS = [\n  {\n    label: \"Weather comparison\",\n    prompt: \"Compare the weather in New York, London, and Tokyo\",\n  },\n  {\n    label: \"GitHub repo stats\",\n    prompt: \"Show me stats for the vercel/next.js and vercel/ai GitHub repos\",\n  },\n  {\n    label: \"Crypto dashboard\",\n    prompt: \"Build a crypto dashboard for Bitcoin, Ethereum, and Solana\",\n  },\n  {\n    label: \"Hacker News top stories\",\n    prompt: \"Show me the top 15 Hacker News stories right now\",\n  },\n];\n\n// =============================================================================\n// Tool Call Display\n// =============================================================================\n\n/** Readable labels for tool names: [loading, done] */\nconst TOOL_LABELS: Record<string, [string, string]> = {\n  getWeather: [\"Getting weather data\", \"Got weather data\"],\n  getGitHubRepo: [\"Fetching GitHub repo\", \"Fetched GitHub repo\"],\n  getGitHubPullRequests: [\"Fetching pull requests\", \"Fetched pull requests\"],\n  getCryptoPrice: [\"Looking up crypto price\", \"Looked up crypto price\"],\n  getCryptoPriceHistory: [\"Fetching price history\", \"Fetched price history\"],\n  getHackerNewsTop: [\"Loading Hacker News\", \"Loaded Hacker News\"],\n  webSearch: [\"Searching the web\", \"Searched the web\"],\n};\n\nfunction ToolCallDisplay({\n  toolName,\n  state,\n  result,\n}: {\n  toolName: string;\n  state: string;\n  result: unknown;\n}) {\n  const [expanded, setExpanded] = useState(false);\n  const isLoading =\n    state !== \"output-available\" &&\n    state !== \"output-error\" &&\n    state !== \"output-denied\";\n  const labels = TOOL_LABELS[toolName];\n  const label = labels ? (isLoading ? labels[0] : labels[1]) : toolName;\n\n  return (\n    <div className=\"text-sm group\">\n      <button\n        type=\"button\"\n        className=\"flex items-center gap-1.5\"\n        onClick={() => setExpanded((e) => !e)}\n      >\n        <span\n          className={`text-muted-foreground ${isLoading ? \"animate-shimmer\" : \"\"}`}\n        >\n          {label}\n        </span>\n        {!isLoading && (\n          <ChevronRight\n            className={`h-3 w-3 text-muted-foreground/0 group-hover:text-muted-foreground transition-all ${expanded ? \"rotate-90\" : \"\"}`}\n          />\n        )}\n      </button>\n      {expanded && !isLoading && result != null && (\n        <div className=\"mt-1 max-h-64 overflow-auto\">\n          <pre className=\"text-xs text-muted-foreground whitespace-pre-wrap break-all\">\n            {typeof result === \"string\"\n              ? result\n              : JSON.stringify(result, null, 2)}\n          </pre>\n        </div>\n      )}\n    </div>\n  );\n}\n\n// =============================================================================\n// Message Bubble\n// =============================================================================\n\nfunction MessageBubble({\n  message,\n  isLast,\n  isStreaming,\n}: {\n  message: AppMessage;\n  isLast: boolean;\n  isStreaming: boolean;\n}) {\n  const isUser = message.role === \"user\";\n  const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);\n\n  // Build ordered segments from parts, collapsing adjacent text and adjacent tools.\n  // Spec data parts are tracked so the rendered UI appears inline where the AI\n  // placed it rather than always at the bottom.\n  const segments: Array<\n    | { kind: \"text\"; text: string }\n    | {\n        kind: \"tools\";\n        tools: Array<{\n          toolCallId: string;\n          toolName: string;\n          state: string;\n          output?: unknown;\n        }>;\n      }\n    | { kind: \"spec\" }\n  > = [];\n\n  let specInserted = false;\n\n  for (const part of message.parts) {\n    if (part.type === \"text\") {\n      if (!part.text.trim()) continue;\n      const last = segments[segments.length - 1];\n      if (last?.kind === \"text\") {\n        last.text += part.text;\n      } else {\n        segments.push({ kind: \"text\", text: part.text });\n      }\n    } else if (part.type.startsWith(\"tool-\")) {\n      const tp = part as {\n        type: string;\n        toolCallId: string;\n        state: string;\n        output?: unknown;\n      };\n      const last = segments[segments.length - 1];\n      if (last?.kind === \"tools\") {\n        last.tools.push({\n          toolCallId: tp.toolCallId,\n          toolName: tp.type.replace(/^tool-/, \"\"),\n          state: tp.state,\n          output: tp.output,\n        });\n      } else {\n        segments.push({\n          kind: \"tools\",\n          tools: [\n            {\n              toolCallId: tp.toolCallId,\n              toolName: tp.type.replace(/^tool-/, \"\"),\n              state: tp.state,\n              output: tp.output,\n            },\n          ],\n        });\n      }\n    } else if (part.type === SPEC_DATA_PART_TYPE && !specInserted) {\n      // First spec data part — mark where the rendered UI should appear\n      segments.push({ kind: \"spec\" });\n      specInserted = true;\n    }\n  }\n\n  const hasAnything = segments.length > 0 || hasSpec;\n  const showLoader =\n    isLast && isStreaming && message.role === \"assistant\" && !hasAnything;\n\n  if (isUser) {\n    return (\n      <div className=\"flex justify-end\">\n        {text && (\n          <div className=\"max-w-[85%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap bg-primary text-primary-foreground rounded-tr-md\">\n            {text}\n          </div>\n        )}\n      </div>\n    );\n  }\n\n  // If there's a spec but no spec segment was inserted (edge case),\n  // append it so it still renders.\n  const specRenderedInline = specInserted;\n  const showSpecAtEnd = hasSpec && !specRenderedInline;\n\n  return (\n    <div className=\"w-full flex flex-col gap-3\">\n      {segments.map((seg, i) => {\n        if (seg.kind === \"text\") {\n          const isLastSegment = i === segments.length - 1;\n          return (\n            <div\n              key={`text-${i}`}\n              className=\"text-sm leading-relaxed [&_p+p]:mt-3 [&_ul]:mt-2 [&_ol]:mt-2 [&_pre]:mt-2\"\n            >\n              <Streamdown\n                plugins={{ code }}\n                animated={isLast && isStreaming && isLastSegment}\n              >\n                {seg.text}\n              </Streamdown>\n            </div>\n          );\n        }\n        if (seg.kind === \"spec\") {\n          if (!hasSpec) return null;\n          return (\n            <div key=\"spec\" className=\"w-full\">\n              <ExplorerRenderer spec={spec} loading={isLast && isStreaming} />\n            </div>\n          );\n        }\n        return (\n          <div key={`tools-${i}`} className=\"flex flex-col gap-1\">\n            {seg.tools.map((t) => (\n              <ToolCallDisplay\n                key={t.toolCallId}\n                toolName={t.toolName}\n                state={t.state}\n                result={t.output}\n              />\n            ))}\n          </div>\n        );\n      })}\n\n      {/* Loading indicator */}\n      {showLoader && (\n        <div className=\"text-sm text-muted-foreground animate-shimmer\">\n          Thinking...\n        </div>\n      )}\n\n      {/* Fallback: render spec at end if no inline position was found */}\n      {showSpecAtEnd && (\n        <div className=\"w-full\">\n          <ExplorerRenderer spec={spec} loading={isLast && isStreaming} />\n        </div>\n      )}\n    </div>\n  );\n}\n\n// =============================================================================\n// Page\n// =============================================================================\n\nexport default function ChatPage() {\n  const [input, setInput] = useState(\"\");\n  const messagesEndRef = useRef<HTMLDivElement>(null);\n  const scrollContainerRef = useRef<HTMLElement>(null);\n  const [showScrollButton, setShowScrollButton] = useState(false);\n  const isStickToBottom = useRef(true);\n  const isAutoScrolling = useRef(false);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const { messages, sendMessage, setMessages, status, error } =\n    useChat<AppMessage>({ transport });\n\n  const isStreaming = status === \"streaming\" || status === \"submitted\";\n\n  // Track whether the user has scrolled away from the bottom.\n  // During programmatic scrolling, suppress button updates until we arrive.\n  useEffect(() => {\n    const container = scrollContainerRef.current;\n    if (!container) return;\n    const THRESHOLD = 80;\n    const handleScroll = () => {\n      const { scrollTop, scrollHeight, clientHeight } = container;\n      const atBottom = scrollTop + clientHeight >= scrollHeight - THRESHOLD;\n\n      if (isAutoScrolling.current) {\n        // Wait for the programmatic scroll to reach the bottom before\n        // handing control back to the user-scroll tracker.\n        if (atBottom) {\n          isAutoScrolling.current = false;\n        }\n        return;\n      }\n\n      isStickToBottom.current = atBottom;\n      setShowScrollButton(!atBottom);\n    };\n    container.addEventListener(\"scroll\", handleScroll, { passive: true });\n    return () => container.removeEventListener(\"scroll\", handleScroll);\n  }, []);\n\n  // Auto-scroll to bottom on new messages, unless user scrolled up.\n  // Uses instant scrollTop assignment (no smooth animation) to avoid\n  // an ongoing animation that fights user scroll input.\n  useEffect(() => {\n    const container = scrollContainerRef.current;\n    if (!container || !isStickToBottom.current) return;\n    isAutoScrolling.current = true;\n    container.scrollTop = container.scrollHeight;\n    requestAnimationFrame(() => {\n      isAutoScrolling.current = false;\n    });\n  }, [messages, isStreaming]);\n\n  const scrollToBottom = useCallback(() => {\n    const container = scrollContainerRef.current;\n    if (!container) return;\n    isStickToBottom.current = true;\n    setShowScrollButton(false);\n    isAutoScrolling.current = true;\n    container.scrollTo({ top: container.scrollHeight, behavior: \"smooth\" });\n    // isAutoScrolling is cleared by the scroll handler once it reaches bottom\n  }, []);\n\n  const handleSubmit = useCallback(\n    async (text?: string) => {\n      const message = text || input;\n      if (!message.trim() || isStreaming) return;\n      setInput(\"\");\n      await sendMessage({ text: message.trim() });\n    },\n    [input, isStreaming, sendMessage],\n  );\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleSubmit();\n      }\n    },\n    [handleSubmit],\n  );\n\n  const handleClear = useCallback(() => {\n    setMessages([]);\n    setInput(\"\");\n    inputRef.current?.focus();\n  }, [setMessages]);\n\n  const isEmpty = messages.length === 0;\n\n  return (\n    <div className=\"h-screen flex flex-col overflow-hidden\">\n      {/* Header */}\n      <header className=\"border-b px-6 py-3 flex items-center justify-between flex-shrink-0\">\n        <div className=\"flex items-center gap-3\">\n          <h1 className=\"text-lg font-semibold\">json-render Chat Example</h1>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          {messages.length > 0 && (\n            <button\n              onClick={handleClear}\n              className=\"px-3 py-1.5 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors\"\n            >\n              Start Over\n            </button>\n          )}\n          <ThemeToggle />\n        </div>\n      </header>\n\n      {/* Messages area */}\n      <main ref={scrollContainerRef} className=\"flex-1 overflow-auto\">\n        {isEmpty ? (\n          /* Empty state */\n          <div className=\"h-full flex flex-col items-center justify-center px-6 py-12\">\n            <div className=\"max-w-2xl w-full space-y-8\">\n              <div className=\"text-center space-y-2\">\n                <h2 className=\"text-2xl font-semibold tracking-tight\">\n                  What would you like to explore?\n                </h2>\n                <p className=\"text-muted-foreground\">\n                  Ask about weather, GitHub repos, crypto prices, or Hacker News\n                  -- the agent will fetch real data and build a dashboard.\n                </p>\n              </div>\n\n              {/* Suggestions */}\n              <div className=\"flex flex-wrap gap-2 justify-center\">\n                {SUGGESTIONS.map((s) => (\n                  <button\n                    key={s.label}\n                    onClick={() => handleSubmit(s.prompt)}\n                    className=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors\"\n                  >\n                    <Sparkles className=\"h-3 w-3\" />\n                    {s.label}\n                  </button>\n                ))}\n              </div>\n            </div>\n          </div>\n        ) : (\n          /* Message thread */\n          <div className=\"max-w-4xl mx-auto px-10 py-6 space-y-6\">\n            {messages.map((message, index) => (\n              <MessageBubble\n                key={message.id}\n                message={message}\n                isLast={index === messages.length - 1}\n                isStreaming={isStreaming}\n              />\n            ))}\n\n            {/* Error display */}\n            {error && (\n              <div className=\"rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n                {error.message}\n              </div>\n            )}\n\n            <div ref={messagesEndRef} />\n          </div>\n        )}\n      </main>\n\n      {/* Input bar - always visible at bottom */}\n      <div className=\"px-6 pb-3 flex-shrink-0 bg-background relative\">\n        {/* Scroll to bottom button */}\n        {showScrollButton && !isEmpty && (\n          <button\n            onClick={scrollToBottom}\n            className=\"absolute left-1/2 -translate-x-1/2 -top-10 z-10 h-8 w-8 rounded-full border border-border bg-background text-muted-foreground shadow-md flex items-center justify-center hover:text-foreground hover:bg-accent transition-colors\"\n            aria-label=\"Scroll to bottom\"\n          >\n            <ArrowDown className=\"h-4 w-4\" />\n          </button>\n        )}\n        <div className=\"max-w-4xl mx-auto relative\">\n          <textarea\n            ref={inputRef}\n            value={input}\n            onChange={(e) => setInput(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder={\n              isEmpty\n                ? \"e.g., Compare weather in NYC, London, and Tokyo...\"\n                : \"Ask a follow-up...\"\n            }\n            rows={2}\n            className=\"w-full resize-none rounded-xl border border-input bg-card px-4 py-3 pr-12 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n            autoFocus\n          />\n          <button\n            onClick={() => handleSubmit()}\n            disabled={!input.trim() || isStreaming}\n            className=\"absolute right-3 bottom-3 h-8 w-8 rounded-lg bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n          >\n            {isStreaming ? (\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            ) : (\n              <ArrowUp className=\"h-4 w-4\" />\n            )}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/chat/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n  return (\n    <NextThemesProvider\n      attribute=\"class\"\n      defaultTheme=\"system\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      {children}\n    </NextThemesProvider>\n  );\n}\n"
  },
  {
    "path": "examples/chat/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <button className=\"p-2 rounded-md border border-border bg-card\">\n        <Sun className=\"h-4 w-4\" />\n      </button>\n    );\n  }\n\n  return (\n    <button\n      onClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n      className=\"p-2 rounded-md border border-border bg-card hover:bg-accent transition-colors\"\n      aria-label=\"Toggle theme\"\n    >\n      {theme === \"dark\" ? (\n        <Sun className=\"h-4 w-4\" />\n      ) : (\n        <Moon className=\"h-4 w-4\" />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "examples/chat/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\nimport { ChevronDownIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Accordion({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return (\n    <AccordionPrimitive.Root\n      data-slot=\"accordion\"\n      className={cn(\"w-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] [&[data-state=open]>svg]:rotate-180\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n      {...props}\n    >\n      <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "examples/chat/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "examples/chat/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        ghost: \"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 [a&]:hover:underline\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      data-variant={variant}\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "examples/chat/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\ninterface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot.Root : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\nexport type { ButtonProps };\n"
  },
  {
    "path": "examples/chat/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "examples/chat/components/ui/chart.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig;\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"];\n}) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color,\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join(\"\\n\")}\n}\n`,\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n  }) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ]);\n\n  if (!active || !payload?.length) {\n    return null;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n            const indicatorColor = color || item.payload.fill || item.color;\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                  indicator === \"dot\" && \"items-center\",\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            },\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\",\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n  }) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\",\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "examples/chat/components/ui/input.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Input = React.forwardRef<\n  HTMLInputElement,\n  React.InputHTMLAttributes<HTMLInputElement>\n>(({ className, type, ...props }, ref) => (\n  <input\n    type={type}\n    className={cn(\n      \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n      className,\n    )}\n    ref={ref}\n    {...props}\n  />\n));\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "examples/chat/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\nimport { cn } from \"@/lib/utils\";\n\nconst Label = React.forwardRef<\n  React.ComponentRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n      className,\n    )}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "examples/chat/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Progress({\n  className,\n  value,\n  ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\n        \"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator\n        data-slot=\"progress-indicator\"\n        className=\"bg-primary h-full w-full flex-1 transition-all\"\n        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n      />\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport { Progress };\n"
  },
  {
    "path": "examples/chat/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport { Circle } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <RadioGroupPrimitive.Root\n    className={cn(\"grid gap-2\", className)}\n    {...props}\n    ref={ref}\n  />\n));\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <RadioGroupPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n      <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n    </RadioGroupPrimitive.Indicator>\n  </RadioGroupPrimitive.Item>\n));\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "examples/chat/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\nconst SelectGroup = SelectPrimitive.Group;\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectItem,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "examples/chat/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "examples/chat/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "examples/chat/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "examples/chat/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst tabsListVariants = cva(\n  \"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction TabsList({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n  VariantProps<typeof tabsListVariants>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground\",\n        \"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n"
  },
  {
    "path": "examples/chat/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      \"react/prop-types\": \"off\",\n      \"react/no-unknown-property\": [\n        \"error\",\n        {\n          ignore: [\n            \"jsx\",\n            // React Three Fiber properties\n            \"args\",\n            \"position\",\n            \"rotation\",\n            \"intensity\",\n            \"distance\",\n            \"metalness\",\n            \"roughness\",\n            \"emissive\",\n            \"emissiveIntensity\",\n            \"wireframe\",\n            \"transparent\",\n          ],\n        },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "examples/chat/lib/agent.ts",
    "content": "import { ToolLoopAgent, stepCountIs } from \"ai\";\nimport { gateway } from \"@ai-sdk/gateway\";\nimport { explorerCatalog } from \"./render/catalog\";\nimport { getWeather } from \"./tools/weather\";\nimport { getGitHubRepo, getGitHubPullRequests } from \"./tools/github\";\nimport { getCryptoPrice, getCryptoPriceHistory } from \"./tools/crypto\";\nimport { getHackerNewsTop } from \"./tools/hackernews\";\nimport { webSearch } from \"./tools/search\";\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nconst AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.\n\nWORKFLOW:\n1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.\n2. Respond with a brief, conversational summary of what you found.\n3. Then output the JSONL UI spec wrapped in a \\`\\`\\`spec fence to render a rich visual experience.\n\nRULES:\n- Always call tools FIRST to get real data. Never make up data.\n- Embed the fetched data directly in /state paths so components can reference it.\n- Use Card components to group related information.\n- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.\n- Use Grid for multi-column layouts.\n- Use Metric for key numeric values (temperature, stars, price, etc.).\n- Use Table for lists of items (stories, forecasts, languages, etc.).\n- Use BarChart or LineChart for numeric trends and time-series data.\n- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).\n- Use Tabs when showing multiple categories of data side by side.\n- Use Badge for status indicators.\n- Use Callout for key facts, tips, warnings, or important takeaways.\n- Use Accordion to organize detailed sections the user can expand for deeper reading.\n- Use Timeline for historical events, processes, step-by-step explanations, or milestones.\n- When teaching about a topic, combine multiple component types to create a rich, engaging experience.\n\n3D SCENES:\nYou can build interactive 3D scenes using React Three Fiber primitives. Use these when the user asks about spatial/visual topics (solar system, molecules, geometry, architecture, physics, etc.).\n\nSCENE STRUCTURE:\n- Scene3D is the root container. ALL other 3D components must be descendants of a Scene3D.\n- Set height (CSS string like \"500px\"), background color, and cameraPosition [x,y,z].\n- Scene3D includes orbit controls so users can rotate, zoom, and pan the camera.\n\n3D PRIMITIVES:\n- Sphere, Box, Cylinder, Cone, Torus, Plane, Ring — geometry meshes with built-in materials.\n- All accept: position [x,y,z], rotation [x,y,z], scale [x,y,z], color, args (geometry dimensions), metalness, roughness, emissive, emissiveIntensity, wireframe, opacity.\n- args vary per geometry: Sphere [radius, wSeg, hSeg], Box [w, h, d], Cylinder [rTop, rBot, h, seg], Ring [inner, outer, seg], etc.\n- Use emissive + emissiveIntensity for glowing objects (like stars/suns).\n\nGROUPING & ANIMATION:\n- Group3D groups children and applies shared transform + animation.\n- animation: { rotate: [x, y, z] } — continuous rotation speed per frame on each axis.\n- IMPORTANT: Rotation values are applied EVERY FRAME (~60fps). Use very small values! Good orbit speeds are 0.0005 to 0.003. Values above 0.01 look frantic.\n- ORBIT PATTERN: To make an object orbit a center point, put it inside a Group3D with rotation animation. Position the object at its orbital distance from center. The rotating group creates the orbit.\n  Example: Group3D(animation: {rotate: [0, 0.001, 0]}) > Sphere(position: [15, 0, 0]) — the sphere orbits at radius 15.\n- For self-rotation (planet spinning), use animation on the Sphere itself with small values like 0.002-0.005.\n\nLIGHTS:\n- AmbientLight: base illumination for the whole scene (intensity ~0.2-0.5).\n- PointLight: emits from a position in all directions. Use for suns, lamps. Set high intensity (2+) for bright sources.\n- DirectionalLight: parallel rays like sunlight. Position sets direction.\n- Always include at least an AmbientLight so objects are visible.\n\nHELPERS:\n- Stars: starfield background. Use for space scenes. count=5000, fade=true is a good default.\n- Label3D: text in 3D space that always faces the camera. Use to label objects. fontSize ~0.5-1.0 for readable labels.\n- Ring: great for orbit path indicators. Rotate [-1.5708, 0, 0] (i.e. -PI/2) to lay flat, set low opacity (~0.15-0.3).\n\n3D SCENE EXAMPLE (Solar System — all 8 planets):\nScene3D(height=\"500px\", background=\"#000010\", cameraPosition=[0,30,60]) >\n  Stars(count=5000, fade=true)\n  AmbientLight(intensity=0.2)\n  PointLight(position=[0,0,0], intensity=2)\n  Sphere(args=[2.5,32,32], color=\"#FDB813\", emissive=\"#FDB813\", emissiveIntensity=1) — Sun\n  Group3D(animation={rotate:[0,0.003,0]}) > Sphere(position=[5,0,0], args=[0.3,16,16], color=\"#8C7853\") — Mercury\n  Group3D(animation={rotate:[0,0.002,0]}) > Sphere(position=[8,0,0], args=[0.7,16,16], color=\"#FFC649\") — Venus\n  Group3D(animation={rotate:[0,0.0015,0]}) > [Sphere(position=[12,0,0], args=[0.8,16,16], color=\"#4B7BE5\"), Group3D(position=[12,0,0], animation={rotate:[0,0.008,0]}) > Sphere(position=[1.5,0,0], args=[0.2,12,12], color=\"#CCC\")] — Earth + Moon\n  Group3D(animation={rotate:[0,0.001,0]}) > Sphere(position=[16,0,0], args=[0.5,16,16], color=\"#E27B58\") — Mars\n  Group3D(animation={rotate:[0,0.0005,0]}) > Sphere(position=[22,0,0], args=[2,20,20], color=\"#C88B3A\") — Jupiter\n  Group3D(animation={rotate:[0,0.0003,0]}) > Sphere(position=[28,0,0], args=[1.7,20,20], color=\"#FAD5A5\") — Saturn\n  Group3D(animation={rotate:[0,0.0002,0]}) > Sphere(position=[34,0,0], args=[1.2,16,16], color=\"#ACE5EE\") — Uranus\n  Group3D(animation={rotate:[0,0.00015,0]}) > Sphere(position=[40,0,0], args=[1.1,16,16], color=\"#5B5EA6\") — Neptune\n  Ring(rotation=[-1.5708,0,0], args=[inner,outer,64], color=\"#ffffff\", opacity=0.12) for each orbit path\nIMPORTANT: Always include ALL planets when building a solar system. Do not truncate to just 4.\n\nMIXING 2D AND 3D:\n- You can combine 3D scenes with regular 2D components in the same spec. For example, use a Stack or Card at the root with a Scene3D plus Text, Callout, Accordion, etc. as siblings. This lets you build a rich educational experience with both an interactive 3D visualization and text content.\n\nDATA BINDING:\n- The state model is the single source of truth. Put fetched data in /state, then reference it with { \"$state\": \"/json/pointer\" } in any prop.\n- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.\n- Scalar binding: \"title\": { \"$state\": \"/quiz/title\" }\n- Array binding: \"items\": { \"$state\": \"/quiz/questions\" } (for Accordion, Timeline, etc.)\n- For Table, BarChart, LineChart, and PieChart, use { \"$state\": \"/path\" } on the data prop to bind read-only data from state.\n- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.\n- Always use the { \"$state\": \"/foo\" } object syntax for data binding.\n\nINTERACTIVITY:\n- You can use visible, repeat, on.press, and $cond/$then/$else freely.\n- visible: Conditionally show/hide elements based on state. e.g. \"visible\": { \"$state\": \"/q1/answer\", \"eq\": \"a\" }\n- repeat: Iterate over state arrays. e.g. \"repeat\": { \"statePath\": \"/items\" }\n- on.press: Trigger actions on button clicks. e.g. \"on\": { \"press\": { \"action\": \"setState\", \"params\": { \"statePath\": \"/submitted\", \"value\": true } } }\n- $cond/$then/$else: Conditional prop values. e.g. { \"$cond\": { \"$state\": \"/correct\" }, \"$then\": \"Correct!\", \"$else\": \"Try again\" }\n\nBUILT-IN ACTIONS (use with on.press):\n- setState: Set a value at a state path. params: { statePath: \"/foo\", value: \"bar\" }\n- pushState: Append to an array. params: { statePath: \"/items\", value: { ... } }\n- removeState: Remove by index. params: { statePath: \"/items\", index: 0 }\n\nINPUT COMPONENTS:\n- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.\n- SelectInput: Dropdown select. Writes selected value to statePath automatically.\n- TextInput: Text input field. Writes entered value to statePath automatically.\n- Button: Clickable button. Use on.press to trigger actions.\n\nPATTERN — INTERACTIVE QUIZZES:\nWhen the user asks for a quiz, test, or Q&A, build an interactive experience:\n1. Initialize state for each question's answer and submission status:\n   {\"op\":\"add\",\"path\":\"/state/q1\",\"value\":\"\"}\n   {\"op\":\"add\",\"path\":\"/state/q1_submitted\",\"value\":false}\n2. For each question, use a Card with:\n   - A Heading or Text for the question\n   - A RadioGroup with the answer options, writing to /q1, /q2, etc.\n   - A Button with on.press to set the submitted flag: {\"action\":\"setState\",\"params\":{\"statePath\":\"/q1_submitted\",\"value\":true}}\n   - A Text (or Callout) showing feedback, using visible to show only after submission:\n     \"visible\": [{\"$state\":\"/q1_submitted\",\"eq\":true},{\"$state\":\"/q1\",\"eq\":\"correct_value\"}]\n   - Show correct/incorrect feedback using separate visible conditions on different elements.\n3. Example structure per question:\n   Card > Stack(vertical) > [Text(question), RadioGroup(options), Button(Check Answer), Text(Correct! visible when right), Callout(Wrong, visible when wrong & submitted)]\n4. You can also add a final score section that becomes visible when all questions are submitted.\n\n${explorerCatalog.prompt({\n  mode: \"inline\",\n  customRules: [\n    \"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.\",\n    \"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.\",\n    \"Use Metric components for key numbers instead of plain Text.\",\n    \"Put chart data arrays in /state and reference them with { $state: '/path' } on the data prop.\",\n    \"Keep the UI clean and information-dense — no excessive padding or empty space.\",\n    \"For educational prompts ('teach me about', 'explain', 'what is'), use a mix of Callout, Accordion, Timeline, and charts to make the content visually rich.\",\n  ],\n})}`;\n\nexport const agent = new ToolLoopAgent({\n  model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),\n  instructions: AGENT_INSTRUCTIONS,\n  tools: {\n    getWeather,\n    getGitHubRepo,\n    getGitHubPullRequests,\n    getCryptoPrice,\n    getCryptoPriceHistory,\n    getHackerNewsTop,\n    webSearch,\n  },\n  stopWhen: stepCountIs(5),\n  temperature: 0.7,\n});\n"
  },
  {
    "path": "examples/chat/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:chat:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:chat:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/chat/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { z } from \"zod\";\n\n// =============================================================================\n// Shared 3D schemas\n// =============================================================================\n\nconst vec3 = z.array(z.number());\n\nconst animation3D = z\n  .object({\n    rotate: vec3.nullable(),\n  })\n  .nullable();\n\nconst transform3DProps = {\n  position: vec3.nullable(),\n  rotation: vec3.nullable(),\n  scale: vec3.nullable(),\n};\n\nconst material3DProps = {\n  color: z.string().nullable(),\n  metalness: z.number().nullable(),\n  roughness: z.number().nullable(),\n  emissive: z.string().nullable(),\n  emissiveIntensity: z.number().nullable(),\n  wireframe: z.boolean().nullable(),\n  opacity: z.number().nullable(),\n};\n\nconst mesh3DProps = {\n  ...transform3DProps,\n  ...material3DProps,\n  args: z.array(z.number()).nullable(),\n  animation: animation3D,\n};\n\n/**\n * json-render + AI SDK Example Catalog\n *\n * Components for rendering data dashboards generated by the ToolLoopAgent.\n * Data flows in through tools (weather, GitHub, crypto, HN), not user actions.\n */\nexport const explorerCatalog = defineCatalog(schema, {\n  components: {\n    // From @json-render/shadcn (used as-is)\n    Stack: shadcnComponentDefinitions.Stack,\n    Card: shadcnComponentDefinitions.Card,\n    Grid: shadcnComponentDefinitions.Grid,\n    Heading: shadcnComponentDefinitions.Heading,\n    Separator: shadcnComponentDefinitions.Separator,\n    Accordion: shadcnComponentDefinitions.Accordion,\n    Progress: shadcnComponentDefinitions.Progress,\n    Skeleton: shadcnComponentDefinitions.Skeleton,\n    Badge: shadcnComponentDefinitions.Badge,\n    Alert: shadcnComponentDefinitions.Alert,\n\n    // Chat-specific components (different schemas or fully custom)\n    Text: {\n      props: z.object({\n        content: z.string(),\n        muted: z.boolean().nullable(),\n      }),\n      description: \"Text content\",\n      example: { content: \"Here is your data overview.\" },\n    },\n\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        detail: z.string().nullable(),\n        trend: z.enum([\"up\", \"down\", \"neutral\"]).nullable(),\n      }),\n      description:\n        \"Single metric display with label, value, and optional trend indicator\",\n      example: {\n        label: \"Temperature\",\n        value: \"72F\",\n        detail: \"Feels like 68F\",\n        trend: \"up\",\n      },\n    },\n\n    Table: {\n      props: z.object({\n        data: z.array(z.record(z.string(), z.unknown())),\n        columns: z.array(\n          z.object({\n            key: z.string(),\n            label: z.string(),\n          }),\n        ),\n        emptyMessage: z.string().nullable(),\n      }),\n      description:\n        'Data table. Use { \"$state\": \"/path\" } to bind read-only data from state.',\n      example: {\n        data: { $state: \"/stories\" },\n        columns: [\n          { key: \"title\", label: \"Title\" },\n          { key: \"score\", label: \"Score\" },\n        ],\n      },\n    },\n\n    Link: {\n      props: z.object({\n        text: z.string(),\n        href: z.string(),\n      }),\n      description: \"External link that opens in a new tab\",\n      example: { text: \"View on GitHub\", href: \"https://github.com\" },\n    },\n\n    // Charts\n    BarChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Bar chart visualization. Use { \"$state\": \"/path\" } to bind read-only data. xKey is the category field, yKey is the numeric value field.',\n    },\n\n    LineChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Line chart visualization. Use { \"$state\": \"/path\" } to bind read-only data. xKey is the x-axis field, yKey is the numeric value field.',\n    },\n\n    // Interactive\n    Tabs: {\n      props: z.object({\n        defaultValue: z.string().nullable(),\n        tabs: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      slots: [\"default\"],\n      description: \"Tabbed content container\",\n    },\n\n    TabContent: {\n      props: z.object({\n        value: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Content for a specific tab\",\n    },\n\n    // Educational / Rich content\n    Callout: {\n      props: z.object({\n        type: z.enum([\"info\", \"tip\", \"warning\", \"important\"]).nullable(),\n        title: z.string().nullable(),\n        content: z.string(),\n      }),\n      description:\n        \"Highlighted callout box for tips, warnings, notes, or key information\",\n      example: {\n        type: \"tip\",\n        title: \"Did you know?\",\n        content: \"The sun is about 93 million miles from Earth.\",\n      },\n    },\n\n    Timeline: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            title: z.string(),\n            description: z.string().nullable(),\n            date: z.string().nullable(),\n            status: z.enum([\"completed\", \"current\", \"upcoming\"]).nullable(),\n          }),\n        ),\n      }),\n      description:\n        \"Vertical timeline showing ordered events, steps, or historical milestones\",\n      example: {\n        items: [\n          {\n            title: \"Discovery\",\n            description: \"Initial breakthrough\",\n            date: \"1905\",\n            status: \"completed\",\n          },\n        ],\n      },\n    },\n\n    PieChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        nameKey: z.string(),\n        valueKey: z.string(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Pie/donut chart for proportional data. Use { \"$state\": \"/path\" } to bind read-only data. nameKey is the label field, valueKey is the numeric value field.',\n    },\n\n    // Interactive / Input\n    RadioGroup: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      description:\n        'Radio button group for single selection. Use { \"$bindState\": \"/path\" } for two-way binding. Use for multiple-choice questions, settings, or any single-select input.',\n      example: {\n        label: \"Choose one\",\n        value: { $bindState: \"/answer\" },\n        options: [\n          { value: \"a\", label: \"Option A\" },\n          { value: \"b\", label: \"Option B\" },\n        ],\n      },\n    },\n\n    SelectInput: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      description:\n        'Dropdown select input. Use { \"$bindState\": \"/path\" } for two-way binding. Use when there are many options and a dropdown is more compact than radio buttons.',\n      example: {\n        label: \"Country\",\n        value: { $bindState: \"/selectedCountry\" },\n        placeholder: \"Select a country\",\n        options: [\n          { value: \"us\", label: \"United States\" },\n          { value: \"uk\", label: \"United Kingdom\" },\n        ],\n      },\n    },\n\n    TextInput: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        type: z.enum([\"text\", \"email\", \"number\", \"password\", \"url\"]).nullable(),\n      }),\n      description:\n        'Text input field. Use { \"$bindState\": \"/path\" } for two-way binding. Use for free-text entry like names, emails, search, etc.',\n      example: {\n        label: \"Your name\",\n        value: { $bindState: \"/userName\" },\n        placeholder: \"Enter your name\",\n        type: \"text\",\n      },\n    },\n\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z\n          .enum([\"default\", \"secondary\", \"destructive\", \"outline\", \"ghost\"])\n          .nullable(),\n        size: z.enum([\"default\", \"sm\", \"lg\"]).nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Clickable button. Use with on.press to trigger actions like setState, pushState, etc. Can be used for quiz submissions, form actions, navigation, and more.\",\n      example: {\n        label: \"Submit\",\n        variant: \"default\",\n        size: \"default\",\n        disabled: null,\n      },\n    },\n\n    // =========================================================================\n    // 3D Scene Components (React Three Fiber)\n    // =========================================================================\n\n    // Containers\n    Scene3D: {\n      props: z.object({\n        height: z.string().nullable(),\n        background: z.string().nullable(),\n        cameraPosition: vec3.nullable(),\n        cameraFov: z.number().nullable(),\n        autoRotate: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"3D scene container with orbit controls. All 3D components (Sphere, Box, lights, etc.) must be children of a Scene3D. height is a CSS value like '500px'.\",\n      example: {\n        height: \"500px\",\n        background: \"#000010\",\n        cameraPosition: [0, 25, 45],\n        cameraFov: null,\n        autoRotate: null,\n      },\n    },\n\n    Group3D: {\n      props: z.object({\n        ...transform3DProps,\n        animation: animation3D,\n      }),\n      slots: [\"default\"],\n      description:\n        \"3D group for positioning, rotating, and animating children together. Use to create orbits: position a planet inside a Group3D and animate the group's rotation.\",\n      example: {\n        position: null,\n        rotation: null,\n        scale: null,\n        animation: { rotate: [0, 0.005, 0] },\n      },\n    },\n\n    // Geometry primitives\n    Box: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D box/cube mesh. args: [width, height, depth]. Supports on.press for click interaction.\",\n      example: {\n        position: [0, 0, 0],\n        color: \"#4488ff\",\n        args: [1, 1, 1],\n      },\n    },\n\n    Sphere: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D sphere mesh. args: [radius, widthSegments, heightSegments]. Use higher segment counts (32+) for smooth spheres.\",\n      example: {\n        position: [0, 0, 0],\n        color: \"#4B7BE5\",\n        args: [1, 32, 32],\n      },\n    },\n\n    Cylinder: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D cylinder mesh. args: [radiusTop, radiusBottom, height, radialSegments].\",\n      example: {\n        position: [0, 0, 0],\n        color: \"#88aa44\",\n        args: [1, 1, 2, 32],\n      },\n    },\n\n    Cone: {\n      props: z.object(mesh3DProps),\n      description: \"3D cone mesh. args: [radius, height, radialSegments].\",\n      example: {\n        position: [0, 0, 0],\n        color: \"#ff8844\",\n        args: [1, 2, 32],\n      },\n    },\n\n    Torus: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D torus (donut) mesh. args: [radius, tube, radialSegments, tubularSegments].\",\n      example: {\n        position: [0, 0, 0],\n        color: \"#aa44ff\",\n        args: [1, 0.4, 16, 100],\n      },\n    },\n\n    Plane: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D flat plane mesh. args: [width, height]. Useful for ground planes or flat surfaces.\",\n      example: {\n        position: [0, -1, 0],\n        rotation: [-Math.PI / 2, 0, 0],\n        color: \"#334455\",\n        args: [10, 10],\n      },\n    },\n\n    Ring: {\n      props: z.object(mesh3DProps),\n      description:\n        \"3D flat ring mesh. args: [innerRadius, outerRadius, thetaSegments]. Great for orbit path indicators.\",\n      example: {\n        position: [0, 0, 0],\n        rotation: [-Math.PI / 2, 0, 0],\n        color: \"#ffffff\",\n        opacity: 0.2,\n        args: [14.8, 15.2, 64],\n      },\n    },\n\n    // Lights\n    AmbientLight: {\n      props: z.object({\n        color: z.string().nullable(),\n        intensity: z.number().nullable(),\n      }),\n      description:\n        \"Ambient light that illuminates all objects equally. Use for base scene illumination.\",\n      example: { color: null, intensity: 0.3 },\n    },\n\n    PointLight: {\n      props: z.object({\n        position: vec3.nullable(),\n        color: z.string().nullable(),\n        intensity: z.number().nullable(),\n        distance: z.number().nullable(),\n      }),\n      description:\n        \"Point light that emits from a position in all directions. Use for suns, lamps, etc.\",\n      example: { position: [0, 0, 0], intensity: 2 },\n    },\n\n    DirectionalLight: {\n      props: z.object({\n        position: vec3.nullable(),\n        color: z.string().nullable(),\n        intensity: z.number().nullable(),\n      }),\n      description:\n        \"Directional light like sunlight. Position sets direction, not location.\",\n      example: { position: [5, 10, 5], intensity: 1 },\n    },\n\n    // Helpers (drei)\n    Stars: {\n      props: z.object({\n        radius: z.number().nullable(),\n        depth: z.number().nullable(),\n        count: z.number().nullable(),\n        factor: z.number().nullable(),\n        fade: z.boolean().nullable(),\n        speed: z.number().nullable(),\n      }),\n      description:\n        \"Starfield background for space scenes. Renders thousands of tiny points around the scene.\",\n      example: { count: 5000, fade: true },\n    },\n\n    Label3D: {\n      props: z.object({\n        text: z.string(),\n        position: vec3.nullable(),\n        rotation: vec3.nullable(),\n        color: z.string().nullable(),\n        fontSize: z.number().nullable(),\n        anchorX: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n        anchorY: z.enum([\"top\", \"middle\", \"bottom\"]).nullable(),\n      }),\n      description:\n        \"Text label rendered in 3D space. Always faces the camera (billboard). Use for labeling objects in a scene.\",\n      example: {\n        text: \"Earth\",\n        position: [15, 2, 0],\n        color: \"#ffffff\",\n        fontSize: 0.8,\n      },\n    },\n  },\n\n  actions: {},\n});\n"
  },
  {
    "path": "examples/chat/lib/render/registry.tsx",
    "content": "\"use client\";\n\nimport { useState, useRef, type ReactNode } from \"react\";\nimport { useBoundProp, defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\nimport {\n  Bar,\n  BarChart as RechartsBarChart,\n  CartesianGrid,\n  Legend,\n  Line,\n  LineChart as RechartsLineChart,\n  Pie,\n  PieChart as RechartsPieChart,\n  XAxis,\n} from \"recharts\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\";\n\nimport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableHead,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  TrendingUp,\n  TrendingDown,\n  Minus,\n  Info,\n  Lightbulb,\n  AlertTriangle,\n  Star,\n  ArrowUpDown,\n  ArrowUp,\n  ArrowDown,\n} from \"lucide-react\";\n\n// 3D imports\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport {\n  OrbitControls,\n  Stars as DreiStars,\n  Text as DreiText,\n} from \"@react-three/drei\";\nimport type * as THREE from \"three\";\n\nimport { explorerCatalog } from \"./catalog\";\n\n// =============================================================================\n// 3D Helper Types & Components\n// =============================================================================\n\ntype Vec3Tuple = [number, number, number];\n\ninterface Animation3D {\n  rotate?: number[] | null;\n}\n\ninterface Mesh3DProps {\n  position?: number[] | null;\n  rotation?: number[] | null;\n  scale?: number[] | null;\n  color?: string | null;\n  args?: number[] | null;\n  metalness?: number | null;\n  roughness?: number | null;\n  emissive?: string | null;\n  emissiveIntensity?: number | null;\n  wireframe?: boolean | null;\n  opacity?: number | null;\n  animation?: Animation3D | null;\n}\n\nfunction toVec3(v: number[] | null | undefined): Vec3Tuple | undefined {\n  if (!v || v.length < 3) return undefined;\n  return v.slice(0, 3) as Vec3Tuple;\n}\n\nfunction toGeoArgs<T extends unknown[]>(\n  v: number[] | null | undefined,\n  fallback: T,\n): T {\n  if (!v || v.length === 0) return fallback;\n  return v as unknown as T;\n}\n\n/** Shared hook for continuous rotation animation */\nfunction useRotationAnimation(\n  ref: React.RefObject<THREE.Object3D | null>,\n  animation?: Animation3D | null,\n) {\n  useFrame(() => {\n    if (!ref.current || !animation?.rotate) return;\n    const [rx, ry, rz] = animation.rotate;\n    ref.current.rotation.x += rx ?? 0;\n    ref.current.rotation.y += ry ?? 0;\n    ref.current.rotation.z += rz ?? 0;\n  });\n}\n\n/** Standard material props shared by all mesh primitives */\nfunction StandardMaterial({\n  color,\n  metalness,\n  roughness,\n  emissive,\n  emissiveIntensity,\n  wireframe,\n  opacity,\n}: Mesh3DProps) {\n  return (\n    <meshStandardMaterial\n      color={color ?? \"#cccccc\"}\n      metalness={metalness ?? 0.1}\n      roughness={roughness ?? 0.8}\n      emissive={emissive ?? undefined}\n      emissiveIntensity={emissiveIntensity ?? 1}\n      wireframe={wireframe ?? false}\n      transparent={opacity != null && opacity < 1}\n      opacity={opacity ?? 1}\n    />\n  );\n}\n\n/** Generic mesh wrapper for all geometry primitives */\nfunction MeshPrimitive({\n  meshProps,\n  children,\n  onClick,\n}: {\n  meshProps: Mesh3DProps;\n  children: ReactNode;\n  onClick?: () => void;\n}) {\n  const ref = useRef<THREE.Mesh>(null);\n  useRotationAnimation(ref, meshProps.animation);\n  return (\n    <mesh\n      ref={ref}\n      position={toVec3(meshProps.position)}\n      rotation={toVec3(meshProps.rotation)}\n      scale={toVec3(meshProps.scale)}\n      onClick={onClick}\n    >\n      {children}\n      <StandardMaterial {...meshProps} />\n    </mesh>\n  );\n}\n\n/** Animated group wrapper */\nfunction AnimatedGroup({\n  position,\n  rotation,\n  scale,\n  animation,\n  children,\n}: {\n  position?: number[] | null;\n  rotation?: number[] | null;\n  scale?: number[] | null;\n  animation?: Animation3D | null;\n  children?: ReactNode;\n}) {\n  const ref = useRef<THREE.Group>(null);\n  useRotationAnimation(ref, animation);\n  return (\n    <group\n      ref={ref}\n      position={toVec3(position)}\n      rotation={toVec3(rotation)}\n      scale={toVec3(scale)}\n    >\n      {children}\n    </group>\n  );\n}\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport const { registry, handlers } = defineRegistry(explorerCatalog, {\n  components: {\n    // From @json-render/shadcn (used as-is)\n    Stack: shadcnComponents.Stack,\n    Card: shadcnComponents.Card,\n    Grid: shadcnComponents.Grid,\n    Heading: shadcnComponents.Heading,\n    Separator: shadcnComponents.Separator,\n    Accordion: shadcnComponents.Accordion,\n    Progress: shadcnComponents.Progress,\n    Skeleton: shadcnComponents.Skeleton,\n    Badge: shadcnComponents.Badge,\n    Alert: shadcnComponents.Alert,\n\n    // Chat-specific components\n    Text: ({ props }) => (\n      <p className={props.muted ? \"text-muted-foreground\" : \"\"}>\n        {props.content}\n      </p>\n    ),\n\n    Metric: ({ props }) => {\n      const TrendIcon =\n        props.trend === \"up\"\n          ? TrendingUp\n          : props.trend === \"down\"\n            ? TrendingDown\n            : Minus;\n      const trendColor =\n        props.trend === \"up\"\n          ? \"text-green-500\"\n          : props.trend === \"down\"\n            ? \"text-red-500\"\n            : \"text-muted-foreground\";\n      return (\n        <div className=\"flex flex-col gap-1\">\n          <p className=\"text-sm text-muted-foreground\">{props.label}</p>\n          <div className=\"flex items-center gap-2\">\n            <span className=\"text-2xl font-bold\">{props.value}</span>\n            {props.trend && <TrendIcon className={`h-4 w-4 ${trendColor}`} />}\n          </div>\n          {props.detail && (\n            <p className=\"text-xs text-muted-foreground\">{props.detail}</p>\n          )}\n        </div>\n      );\n    },\n\n    Table: ({ props }) => {\n      const rawData = props.data;\n      const items: Array<Record<string, unknown>> = Array.isArray(rawData)\n        ? rawData\n        : Array.isArray((rawData as Record<string, unknown>)?.data)\n          ? ((rawData as Record<string, unknown>).data as Array<\n              Record<string, unknown>\n            >)\n          : [];\n\n      const [sortKey, setSortKey] = useState<string | null>(null);\n      const [sortDir, setSortDir] = useState<\"asc\" | \"desc\">(\"asc\");\n\n      if (items.length === 0) {\n        return (\n          <div className=\"text-center py-4 text-muted-foreground\">\n            {props.emptyMessage ?? \"No data\"}\n          </div>\n        );\n      }\n\n      const sorted = sortKey\n        ? [...items].sort((a, b) => {\n            const av = a[sortKey];\n            const bv = b[sortKey];\n            // numeric comparison when both values are numbers\n            if (typeof av === \"number\" && typeof bv === \"number\") {\n              return sortDir === \"asc\" ? av - bv : bv - av;\n            }\n            const as = String(av ?? \"\");\n            const bs = String(bv ?? \"\");\n            return sortDir === \"asc\"\n              ? as.localeCompare(bs)\n              : bs.localeCompare(as);\n          })\n        : items;\n\n      const handleSort = (key: string) => {\n        if (sortKey === key) {\n          setSortDir((d) => (d === \"asc\" ? \"desc\" : \"asc\"));\n        } else {\n          setSortKey(key);\n          setSortDir(\"asc\");\n        }\n      };\n\n      return (\n        <Table>\n          <TableHeader>\n            <TableRow>\n              {props.columns.map((col) => {\n                const SortIcon =\n                  sortKey === col.key\n                    ? sortDir === \"asc\"\n                      ? ArrowUp\n                      : ArrowDown\n                    : ArrowUpDown;\n                return (\n                  <TableHead key={col.key}>\n                    <button\n                      type=\"button\"\n                      className=\"inline-flex items-center gap-1 hover:text-foreground transition-colors\"\n                      onClick={() => handleSort(col.key)}\n                    >\n                      {col.label}\n                      <SortIcon className=\"h-3 w-3 text-muted-foreground\" />\n                    </button>\n                  </TableHead>\n                );\n              })}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {sorted.map((item, i) => (\n              <TableRow key={i}>\n                {props.columns.map((col) => (\n                  <TableCell key={col.key}>\n                    {String(item[col.key] ?? \"\")}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      );\n    },\n\n    Link: ({ props }) => (\n      <a\n        href={props.href}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"text-primary underline underline-offset-4 hover:text-primary/80\"\n      >\n        {props.text}\n      </a>\n    ),\n\n    BarChart: ({ props }) => {\n      const rawData = props.data;\n      const rawItems: Array<Record<string, unknown>> = Array.isArray(rawData)\n        ? rawData\n        : Array.isArray((rawData as Record<string, unknown>)?.data)\n          ? ((rawData as Record<string, unknown>).data as Array<\n              Record<string, unknown>\n            >)\n          : [];\n\n      const { items, valueKey } = processChartData(\n        rawItems,\n        props.xKey,\n        props.yKey,\n        props.aggregate,\n      );\n\n      const chartColor = props.color ?? \"var(--chart-1)\";\n\n      const chartConfig = {\n        [valueKey]: {\n          label: valueKey,\n          color: chartColor,\n        },\n      } satisfies ChartConfig;\n\n      if (items.length === 0) {\n        return (\n          <div className=\"text-center py-4 text-muted-foreground\">\n            No data available\n          </div>\n        );\n      }\n\n      return (\n        <div className=\"w-full\">\n          {props.title && (\n            <p className=\"text-sm font-medium mb-2\">{props.title}</p>\n          )}\n          <ChartContainer\n            config={chartConfig}\n            className=\"min-h-[200px] w-full\"\n            style={{ height: props.height ?? 300 }}\n          >\n            <RechartsBarChart accessibilityLayer data={items}>\n              <CartesianGrid vertical={false} />\n              <XAxis\n                dataKey=\"label\"\n                tickLine={false}\n                tickMargin={10}\n                axisLine={false}\n              />\n              <ChartTooltip content={<ChartTooltipContent />} />\n              <Bar\n                dataKey={valueKey}\n                fill={`var(--color-${valueKey})`}\n                radius={4}\n              />\n            </RechartsBarChart>\n          </ChartContainer>\n        </div>\n      );\n    },\n\n    LineChart: ({ props }) => {\n      const rawData = props.data;\n      const rawItems: Array<Record<string, unknown>> = Array.isArray(rawData)\n        ? rawData\n        : Array.isArray((rawData as Record<string, unknown>)?.data)\n          ? ((rawData as Record<string, unknown>).data as Array<\n              Record<string, unknown>\n            >)\n          : [];\n\n      const { items, valueKey } = processChartData(\n        rawItems,\n        props.xKey,\n        props.yKey,\n        props.aggregate,\n      );\n\n      const chartColor = props.color ?? \"var(--chart-1)\";\n\n      const chartConfig = {\n        [valueKey]: {\n          label: valueKey,\n          color: chartColor,\n        },\n      } satisfies ChartConfig;\n\n      if (items.length === 0) {\n        return (\n          <div className=\"text-center py-4 text-muted-foreground\">\n            No data available\n          </div>\n        );\n      }\n\n      return (\n        <div className=\"w-full\">\n          {props.title && (\n            <p className=\"text-sm font-medium mb-2\">{props.title}</p>\n          )}\n          <ChartContainer\n            config={chartConfig}\n            className=\"min-h-[200px] w-full [&_svg]:overflow-visible\"\n            style={{ height: props.height ?? 300 }}\n          >\n            <RechartsLineChart accessibilityLayer data={items}>\n              <CartesianGrid vertical={false} />\n              <XAxis\n                dataKey=\"label\"\n                tickLine={false}\n                tickMargin={10}\n                axisLine={false}\n                interval={\n                  items.length > 12\n                    ? Math.ceil(items.length / 8) - 1\n                    : undefined\n                }\n              />\n              <ChartTooltip content={<ChartTooltipContent />} />\n              <Line\n                type=\"monotone\"\n                dataKey={valueKey}\n                stroke={`var(--color-${valueKey})`}\n                strokeWidth={2}\n                dot={false}\n              />\n            </RechartsLineChart>\n          </ChartContainer>\n        </div>\n      );\n    },\n\n    Tabs: ({ props, children }) => (\n      <Tabs defaultValue={props.defaultValue ?? (props.tabs ?? [])[0]?.value}>\n        <TabsList>\n          {(props.tabs ?? []).map((tab) => (\n            <TabsTrigger key={tab.value} value={tab.value}>\n              {tab.label}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n        {children}\n      </Tabs>\n    ),\n\n    TabContent: ({ props, children }) => (\n      <TabsContent value={props.value}>{children}</TabsContent>\n    ),\n\n    Callout: ({ props }) => {\n      const config = {\n        info: {\n          icon: Info,\n          border: \"border-l-blue-500\",\n          bg: \"bg-blue-500/5\",\n          iconColor: \"text-blue-500\",\n        },\n        tip: {\n          icon: Lightbulb,\n          border: \"border-l-emerald-500\",\n          bg: \"bg-emerald-500/5\",\n          iconColor: \"text-emerald-500\",\n        },\n        warning: {\n          icon: AlertTriangle,\n          border: \"border-l-amber-500\",\n          bg: \"bg-amber-500/5\",\n          iconColor: \"text-amber-500\",\n        },\n        important: {\n          icon: Star,\n          border: \"border-l-purple-500\",\n          bg: \"bg-purple-500/5\",\n          iconColor: \"text-purple-500\",\n        },\n      }[props.type ?? \"info\"] ?? {\n        icon: Info,\n        border: \"border-l-blue-500\",\n        bg: \"bg-blue-500/5\",\n        iconColor: \"text-blue-500\",\n      };\n      const Icon = config.icon;\n      return (\n        <div\n          className={`border-l-4 ${config.border} ${config.bg} rounded-r-lg p-4`}\n        >\n          <div className=\"flex items-start gap-3\">\n            <Icon className={`h-5 w-5 mt-0.5 shrink-0 ${config.iconColor}`} />\n            <div className=\"flex-1 min-w-0\">\n              {props.title && (\n                <p className=\"font-semibold text-sm mb-1\">{props.title}</p>\n              )}\n              <p className=\"text-sm text-muted-foreground\">{props.content}</p>\n            </div>\n          </div>\n        </div>\n      );\n    },\n\n    Timeline: ({ props }) => (\n      <div className=\"relative pl-8\">\n        {/* Vertical line centered on dots: dot is 12px wide starting at 0px, center = 6px */}\n        <div className=\"absolute left-[5.5px] top-3 bottom-3 w-px bg-border\" />\n        <div className=\"flex flex-col gap-6\">\n          {(props.items ?? []).map((item, i) => {\n            const dotColor =\n              item.status === \"completed\"\n                ? \"bg-emerald-500\"\n                : item.status === \"current\"\n                  ? \"bg-blue-500\"\n                  : \"bg-muted-foreground/30\";\n            return (\n              <div key={i} className=\"relative\">\n                <div\n                  className={`absolute -left-8 top-0.5 h-3 w-3 rounded-full ${dotColor} ring-2 ring-background`}\n                />\n                <div className=\"flex-1 min-w-0\">\n                  <div className=\"flex items-center gap-2 flex-wrap\">\n                    <p className=\"font-medium text-sm\">{item.title}</p>\n                    {item.date && (\n                      <span className=\"text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n                        {item.date}\n                      </span>\n                    )}\n                  </div>\n                  {item.description && (\n                    <p className=\"text-sm text-muted-foreground mt-1\">\n                      {item.description}\n                    </p>\n                  )}\n                </div>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    ),\n\n    PieChart: ({ props }) => {\n      const rawData = props.data;\n      const items: Array<Record<string, unknown>> = Array.isArray(rawData)\n        ? rawData\n        : Array.isArray((rawData as Record<string, unknown>)?.data)\n          ? ((rawData as Record<string, unknown>).data as Array<\n              Record<string, unknown>\n            >)\n          : [];\n\n      if (items.length === 0) {\n        return (\n          <div className=\"text-center py-4 text-muted-foreground\">\n            No data available\n          </div>\n        );\n      }\n\n      const chartConfig: ChartConfig = {};\n      items.forEach((item, i) => {\n        const name = String(item[props.nameKey] ?? `Segment ${i + 1}`);\n        chartConfig[name] = {\n          label: name,\n          color: PIE_COLORS[i % PIE_COLORS.length],\n        };\n      });\n\n      return (\n        <div className=\"w-full\">\n          {props.title && (\n            <p className=\"text-sm font-medium mb-2\">{props.title}</p>\n          )}\n          <ChartContainer\n            config={chartConfig}\n            className=\"mx-auto aspect-square w-full\"\n            style={{ height: props.height ?? 300 }}\n          >\n            <RechartsPieChart>\n              <ChartTooltip content={<ChartTooltipContent />} />\n              <Pie\n                data={items.map((item, i) => ({\n                  name: String(item[props.nameKey] ?? `Segment ${i + 1}`),\n                  value:\n                    typeof item[props.valueKey] === \"number\"\n                      ? item[props.valueKey]\n                      : parseFloat(String(item[props.valueKey])) || 0,\n                  fill: PIE_COLORS[i % PIE_COLORS.length],\n                }))}\n                dataKey=\"value\"\n                nameKey=\"name\"\n                innerRadius=\"40%\"\n                outerRadius=\"70%\"\n                paddingAngle={2}\n              />\n              <Legend />\n            </RechartsPieChart>\n          </ChartContainer>\n        </div>\n      );\n    },\n\n    RadioGroup: ({ props, bindings }) => {\n      const [value, setValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const current = value ?? \"\";\n\n      return (\n        <div className=\"flex flex-col gap-2\">\n          {props.label && (\n            <Label className=\"text-sm font-medium\">{props.label}</Label>\n          )}\n          <RadioGroup\n            value={current}\n            onValueChange={(v: string) => setValue(v)}\n          >\n            {(props.options ?? []).map((opt) => (\n              <div key={opt.value} className=\"flex items-center gap-2\">\n                <RadioGroupItem value={opt.value} id={`rg-${opt.value}`} />\n                <Label\n                  htmlFor={`rg-${opt.value}`}\n                  className=\"font-normal cursor-pointer\"\n                >\n                  {opt.label}\n                </Label>\n              </div>\n            ))}\n          </RadioGroup>\n        </div>\n      );\n    },\n\n    SelectInput: ({ props, bindings }) => {\n      const [value, setValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const current = value ?? \"\";\n\n      return (\n        <div className=\"flex flex-col gap-2\">\n          {props.label && (\n            <Label className=\"text-sm font-medium\">{props.label}</Label>\n          )}\n          <Select value={current} onValueChange={(v: string) => setValue(v)}>\n            <SelectTrigger>\n              <SelectValue placeholder={props.placeholder ?? \"Select...\"} />\n            </SelectTrigger>\n            <SelectContent>\n              {(props.options ?? []).map((opt) => (\n                <SelectItem key={opt.value} value={opt.value}>\n                  {opt.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n      );\n    },\n\n    TextInput: ({ props, bindings }) => {\n      const [value, setValue] = useBoundProp<string>(\n        props.value as string | undefined,\n        bindings?.value,\n      );\n      const current = value ?? \"\";\n\n      return (\n        <div className=\"flex flex-col gap-2\">\n          {props.label && (\n            <Label className=\"text-sm font-medium\">{props.label}</Label>\n          )}\n          <Input\n            type={props.type ?? \"text\"}\n            placeholder={props.placeholder ?? \"\"}\n            value={current}\n            onChange={(e) => setValue(e.target.value)}\n          />\n        </div>\n      );\n    },\n\n    Button: ({ props, emit }) => (\n      <Button\n        variant={props.variant ?? \"default\"}\n        size={props.size ?? \"default\"}\n        disabled={props.disabled ?? false}\n        onClick={() => emit(\"press\")}\n      >\n        {props.label}\n      </Button>\n    ),\n\n    // =========================================================================\n    // 3D Scene Components\n    // =========================================================================\n\n    Scene3D: ({ props, children }) => (\n      <div\n        style={{\n          height: props.height ?? \"400px\",\n          width: \"100%\",\n          background: props.background ?? \"#111111\",\n          borderRadius: 8,\n          overflow: \"hidden\",\n        }}\n      >\n        <Canvas\n          camera={{\n            position: toVec3(props.cameraPosition) ?? [0, 10, 30],\n            fov: props.cameraFov ?? 50,\n          }}\n        >\n          <OrbitControls\n            autoRotate={props.autoRotate ?? false}\n            enablePan\n            enableZoom\n          />\n          {children}\n        </Canvas>\n      </div>\n    ),\n\n    Group3D: ({ props, children }) => (\n      <AnimatedGroup\n        position={props.position}\n        rotation={props.rotation}\n        scale={props.scale}\n        animation={props.animation}\n      >\n        {children}\n      </AnimatedGroup>\n    ),\n\n    Box: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <boxGeometry\n          args={toGeoArgs<[number, number, number]>(props.args, [1, 1, 1])}\n        />\n      </MeshPrimitive>\n    ),\n\n    Sphere: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <sphereGeometry\n          args={toGeoArgs<[number, number, number]>(props.args, [1, 32, 32])}\n        />\n      </MeshPrimitive>\n    ),\n\n    Cylinder: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <cylinderGeometry\n          args={toGeoArgs<[number, number, number, number]>(\n            props.args,\n            [1, 1, 2, 32],\n          )}\n        />\n      </MeshPrimitive>\n    ),\n\n    Cone: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <coneGeometry\n          args={toGeoArgs<[number, number, number]>(props.args, [1, 2, 32])}\n        />\n      </MeshPrimitive>\n    ),\n\n    Torus: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <torusGeometry\n          args={toGeoArgs<[number, number, number, number]>(\n            props.args,\n            [1, 0.4, 16, 100],\n          )}\n        />\n      </MeshPrimitive>\n    ),\n\n    Plane: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <planeGeometry\n          args={toGeoArgs<[number, number]>(props.args, [10, 10])}\n        />\n      </MeshPrimitive>\n    ),\n\n    Ring: ({ props, emit }) => (\n      <MeshPrimitive meshProps={props} onClick={() => emit(\"press\")}>\n        <ringGeometry\n          args={toGeoArgs<[number, number, number]>(props.args, [0.5, 1, 64])}\n        />\n      </MeshPrimitive>\n    ),\n\n    AmbientLight: ({ props }) => (\n      <ambientLight\n        color={props.color ?? undefined}\n        intensity={props.intensity ?? 0.5}\n      />\n    ),\n\n    PointLight: ({ props }) => (\n      <pointLight\n        position={toVec3(props.position)}\n        color={props.color ?? undefined}\n        intensity={props.intensity ?? 1}\n        distance={props.distance ?? 0}\n      />\n    ),\n\n    DirectionalLight: ({ props }) => (\n      <directionalLight\n        position={toVec3(props.position)}\n        color={props.color ?? undefined}\n        intensity={props.intensity ?? 1}\n      />\n    ),\n\n    Stars: ({ props }) => (\n      <DreiStars\n        radius={props.radius ?? 100}\n        depth={props.depth ?? 50}\n        count={props.count ?? 5000}\n        factor={props.factor ?? 4}\n        fade={props.fade ?? true}\n        speed={props.speed ?? 1}\n      />\n    ),\n\n    Label3D: ({ props }) => (\n      <DreiText\n        position={toVec3(props.position)}\n        rotation={toVec3(props.rotation)}\n        color={props.color ?? \"#ffffff\"}\n        fontSize={props.fontSize ?? 1}\n        anchorX={props.anchorX ?? \"center\"}\n        anchorY={props.anchorY ?? \"middle\"}\n      >\n        {props.text}\n      </DreiText>\n    ),\n  },\n});\n\n// =============================================================================\n// Chart Helpers\n// =============================================================================\n\nconst PIE_COLORS = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n];\n\nfunction processChartData(\n  items: Array<Record<string, unknown>>,\n  xKey: string,\n  yKey: string,\n  aggregate: \"sum\" | \"count\" | \"avg\" | null | undefined,\n): { items: Array<Record<string, unknown>>; valueKey: string } {\n  if (items.length === 0) {\n    return { items: [], valueKey: yKey };\n  }\n\n  if (!aggregate) {\n    const formatted = items.map((item) => ({\n      ...item,\n      label: String(item[xKey] ?? \"\"),\n    }));\n    return { items: formatted, valueKey: yKey };\n  }\n\n  const groups = new Map<string, Array<Record<string, unknown>>>();\n\n  for (const item of items) {\n    const groupKey = String(item[xKey] ?? \"unknown\");\n    const group = groups.get(groupKey) ?? [];\n    group.push(item);\n    groups.set(groupKey, group);\n  }\n\n  const valueKey = aggregate === \"count\" ? \"count\" : yKey;\n  const aggregated: Array<Record<string, unknown>> = [];\n  const sortedKeys = Array.from(groups.keys()).sort();\n\n  for (const key of sortedKeys) {\n    const group = groups.get(key)!;\n    let value: number;\n\n    if (aggregate === \"count\") {\n      value = group.length;\n    } else if (aggregate === \"sum\") {\n      value = group.reduce((sum, item) => {\n        const v = item[yKey];\n        return sum + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n      }, 0);\n    } else {\n      const sum = group.reduce((s, item) => {\n        const v = item[yKey];\n        return s + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n      }, 0);\n      value = group.length > 0 ? sum / group.length : 0;\n    }\n\n    aggregated.push({ label: key, [valueKey]: value });\n  }\n\n  return { items: aggregated, valueKey };\n}\n\n// =============================================================================\n// Fallback Component\n// =============================================================================\n\nexport function Fallback({ type }: { type: string }) {\n  return (\n    <div className=\"p-4 border border-dashed rounded-lg text-muted-foreground text-sm\">\n      Unknown component: {type}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/chat/lib/render/renderer.tsx",
    "content": "\"use client\";\n\nimport { type ReactNode } from \"react\";\nimport {\n  Renderer,\n  type ComponentRenderer,\n  type Spec,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n} from \"@json-render/react\";\n\nimport { registry, Fallback } from \"./registry\";\n\n// =============================================================================\n// ExplorerRenderer\n// =============================================================================\n\ninterface ExplorerRendererProps {\n  spec: Spec | null;\n  loading?: boolean;\n}\n\nconst fallback: ComponentRenderer = ({ element }) => (\n  <Fallback type={element.type} />\n);\n\nexport function ExplorerRenderer({\n  spec,\n  loading,\n}: ExplorerRendererProps): ReactNode {\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={spec.state ?? {}}>\n      <VisibilityProvider>\n        <ActionProvider>\n          <Renderer\n            spec={spec}\n            registry={registry}\n            fallback={fallback}\n            loading={loading}\n          />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/chat/lib/tools/crypto.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction handleFetchError(res: Response, coinId: string) {\n  if (res.status === 404) {\n    return { error: `Cryptocurrency not found: ${coinId}` };\n  }\n  if (res.status === 429) {\n    return { error: \"CoinGecko rate limit exceeded. Try again in a minute.\" };\n  }\n  return { error: `Failed to fetch crypto data: ${res.statusText}` };\n}\n\nfunction sampleTimeSeries(\n  prices: [number, number][],\n  maxPoints: number,\n): Array<{ date: string; price: number }> {\n  const step = Math.max(1, Math.floor(prices.length / maxPoints));\n  return prices\n    .filter((_, i) => i % step === 0)\n    .map(([timestamp, price]) => ({\n      date: new Date(timestamp).toLocaleDateString(\"en-US\", {\n        month: \"short\",\n        day: \"numeric\",\n      }),\n      price: Math.round(price * 100) / 100,\n    }));\n}\n\n// =============================================================================\n// getCryptoPrice — current market data + 7-day sparkline\n// =============================================================================\n\n/**\n * Get cryptocurrency market data from CoinGecko.\n * Free public API, no API key required.\n * https://docs.coingecko.com/reference/introduction\n */\nexport const getCryptoPrice = tool({\n  description:\n    \"Get current price, market cap, 24h change, and 7-day sparkline for a cryptocurrency. For longer price history (30d, 90d, 365d), use getCryptoPriceHistory instead.\",\n  inputSchema: z.object({\n    coinId: z\n      .string()\n      .describe(\n        \"CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana', 'dogecoin', 'cardano')\",\n      ),\n  }),\n  execute: async ({ coinId }) => {\n    const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=true`;\n\n    const res = await fetch(url, {\n      headers: { Accept: \"application/json\" },\n    });\n\n    if (!res.ok) return handleFetchError(res, coinId);\n\n    const data = (await res.json()) as {\n      id: string;\n      symbol: string;\n      name: string;\n      market_data: {\n        current_price: { usd: number };\n        market_cap: { usd: number };\n        total_volume: { usd: number };\n        price_change_percentage_24h: number;\n        price_change_percentage_7d: number;\n        price_change_percentage_30d: number;\n        high_24h: { usd: number };\n        low_24h: { usd: number };\n        ath: { usd: number };\n        ath_date: { usd: string };\n        circulating_supply: number;\n        total_supply: number | null;\n        sparkline_7d: { price: number[] };\n      };\n      market_cap_rank: number;\n    };\n\n    const md = data.market_data;\n\n    // Convert sparkline (hourly array) to dated points\n    const now = Date.now();\n    const sparkline = md.sparkline_7d.price;\n    const step = Math.max(1, Math.floor(sparkline.length / 14));\n    const sparklineData = sparkline\n      .filter((_, i) => i % step === 0)\n      .map((price, i) => {\n        const hourIndex = i * step;\n        const ts = now - (sparkline.length - hourIndex) * 3600_000;\n        return {\n          date: new Date(ts).toLocaleDateString(\"en-US\", {\n            month: \"short\",\n            day: \"numeric\",\n          }),\n          price: Math.round(price * 100) / 100,\n        };\n      });\n\n    return {\n      id: data.id,\n      symbol: data.symbol.toUpperCase(),\n      name: data.name,\n      rank: data.market_cap_rank,\n      price: md.current_price.usd,\n      marketCap: md.market_cap.usd,\n      volume24h: md.total_volume.usd,\n      change24h: Math.round(md.price_change_percentage_24h * 100) / 100,\n      change7d: Math.round(md.price_change_percentage_7d * 100) / 100,\n      change30d: Math.round(md.price_change_percentage_30d * 100) / 100,\n      high24h: md.high_24h.usd,\n      low24h: md.low_24h.usd,\n      allTimeHigh: md.ath.usd,\n      allTimeHighDate: md.ath_date.usd,\n      circulatingSupply: md.circulating_supply,\n      totalSupply: md.total_supply,\n      sparkline7d: sparklineData,\n    };\n  },\n});\n\n// =============================================================================\n// getCryptoPriceHistory — flexible date range price history\n// =============================================================================\n\nexport const getCryptoPriceHistory = tool({\n  description:\n    \"Get historical price data for a cryptocurrency over a specified number of days (e.g., 30, 90, 365). Returns date-labeled data points suitable for charting.\",\n  inputSchema: z.object({\n    coinId: z\n      .string()\n      .describe(\"CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana')\"),\n    days: z\n      .number()\n      .int()\n      .min(1)\n      .max(365)\n      .describe(\"Number of days of history to fetch (e.g., 30, 90, 365)\"),\n  }),\n  execute: async ({ coinId, days }) => {\n    const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}/market_chart?vs_currency=usd&days=${days}`;\n\n    const res = await fetch(url, {\n      headers: { Accept: \"application/json\" },\n    });\n\n    if (!res.ok) return handleFetchError(res, coinId);\n\n    const data = (await res.json()) as {\n      prices: [number, number][];\n    };\n\n    const priceHistory = sampleTimeSeries(data.prices, 20);\n\n    return {\n      coinId,\n      days,\n      priceHistory,\n    };\n  },\n});\n"
  },
  {
    "path": "examples/chat/lib/tools/github.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Shared helpers\n// ---------------------------------------------------------------------------\n\nconst ghHeaders = { Accept: \"application/vnd.github.v3+json\" };\n\nfunction handleGitHubError(res: Response, context: string) {\n  if (res.status === 404) return { error: `Not found: ${context}` };\n  if (res.status === 403)\n    return { error: \"GitHub API rate limit exceeded. Try again later.\" };\n  return { error: `Failed to fetch ${context}: ${res.statusText}` };\n}\n\n// ---------------------------------------------------------------------------\n// getGitHubRepo\n// ---------------------------------------------------------------------------\n\n/**\n * Get public GitHub repository information.\n * Uses the public GitHub REST API (no auth, 60 req/hr rate limit).\n */\nexport const getGitHubRepo = tool({\n  description:\n    \"Get information about a public GitHub repository including stars, forks, open issues, description, language, and recent activity.\",\n  inputSchema: z.object({\n    owner: z.string().describe(\"Repository owner (e.g., 'vercel')\"),\n    repo: z.string().describe(\"Repository name (e.g., 'next.js')\"),\n  }),\n  execute: async ({ owner, repo }) => {\n    const repoUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;\n\n    const [repoRes, languagesRes] = await Promise.all([\n      fetch(repoUrl, { headers: ghHeaders }),\n      fetch(`${repoUrl}/languages`, { headers: ghHeaders }),\n    ]);\n\n    if (!repoRes.ok) {\n      return handleGitHubError(repoRes, `${owner}/${repo}`);\n    }\n\n    const repoData = (await repoRes.json()) as {\n      full_name: string;\n      description: string | null;\n      html_url: string;\n      stargazers_count: number;\n      forks_count: number;\n      open_issues_count: number;\n      watchers_count: number;\n      language: string | null;\n      license: { spdx_id: string } | null;\n      created_at: string;\n      updated_at: string;\n      pushed_at: string;\n      topics: string[];\n      size: number;\n      default_branch: string;\n      archived: boolean;\n      fork: boolean;\n    };\n\n    const languages: Record<string, number> = languagesRes.ok\n      ? ((await languagesRes.json()) as Record<string, number>)\n      : {};\n\n    const totalBytes = Object.values(languages).reduce((a, b) => a + b, 0);\n    const languageBreakdown = Object.entries(languages)\n      .map(([lang, bytes]) => ({\n        language: lang,\n        percentage: Math.round((bytes / totalBytes) * 100),\n        bytes,\n      }))\n      .sort((a, b) => b.bytes - a.bytes)\n      .slice(0, 8);\n\n    return {\n      name: repoData.full_name,\n      description: repoData.description,\n      url: repoData.html_url,\n      stars: repoData.stargazers_count,\n      forks: repoData.forks_count,\n      openIssues: repoData.open_issues_count,\n      watchers: repoData.watchers_count,\n      primaryLanguage: repoData.language,\n      license: repoData.license?.spdx_id ?? \"None\",\n      createdAt: repoData.created_at,\n      updatedAt: repoData.updated_at,\n      lastPush: repoData.pushed_at,\n      topics: repoData.topics,\n      defaultBranch: repoData.default_branch,\n      archived: repoData.archived,\n      isFork: repoData.fork,\n      languages: languageBreakdown,\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// getGitHubPullRequests\n// ---------------------------------------------------------------------------\n\ntype GitHubPR = {\n  number: number;\n  title: string;\n  state: string;\n  html_url: string;\n  user: { login: string } | null;\n  created_at: string;\n  updated_at: string;\n  merged_at: string | null;\n  comments: number;\n  labels: Array<{ name: string }>;\n  draft: boolean;\n};\n\ntype GitHubPRReview = {\n  id: number;\n};\n\ntype GitHubPRReaction = {\n  total_count: number;\n};\n\n/**\n * Get pull requests from a public GitHub repository.\n * Supports filtering by state and sorting by various criteria.\n * Fetches comment counts and reactions for ranking \"most popular\" PRs.\n */\nexport const getGitHubPullRequests = tool({\n  description:\n    \"Get pull requests from a public GitHub repository. Returns titles, authors, state, comment counts, and reactions. Use sort='popularity' to find the most discussed / reacted PRs.\",\n  inputSchema: z.object({\n    owner: z.string().describe(\"Repository owner (e.g., 'vercel')\"),\n    repo: z.string().describe(\"Repository name (e.g., 'next.js')\"),\n    state: z\n      .enum([\"open\", \"closed\", \"all\"])\n      .nullable()\n      .describe(\"Filter by state. Defaults to 'open'.\"),\n    sort: z\n      .enum([\"created\", \"updated\", \"popularity\", \"long-running\"])\n      .nullable()\n      .describe(\n        \"Sort order. 'popularity' sorts by reactions+comments, 'long-running' sorts by age. Defaults to 'created'.\",\n      ),\n    perPage: z\n      .number()\n      .int()\n      .min(1)\n      .max(30)\n      .nullable()\n      .describe(\"Number of PRs to return (1-30). Defaults to 10.\"),\n  }),\n  execute: async ({ owner, repo, state, sort, perPage }) => {\n    const count = perPage ?? 10;\n    const prState = state ?? \"open\";\n\n    // GitHub API sort param: 'popularity' and 'long-running' are API-native\n    const apiSort =\n      sort === \"popularity\"\n        ? \"popularity\"\n        : sort === \"long-running\"\n          ? \"long-running\"\n          : sort === \"updated\"\n            ? \"updated\"\n            : \"created\";\n\n    const url = new URL(\n      `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`,\n    );\n    url.searchParams.set(\"state\", prState);\n    url.searchParams.set(\"sort\", apiSort);\n    url.searchParams.set(\"direction\", \"desc\");\n    url.searchParams.set(\"per_page\", String(count));\n\n    const res = await fetch(url.toString(), { headers: ghHeaders });\n\n    if (!res.ok) {\n      return handleGitHubError(res, `${owner}/${repo} pull requests`);\n    }\n\n    const prs = (await res.json()) as GitHubPR[];\n\n    // Fetch review + reaction counts in parallel for richer data\n    const enriched = await Promise.all(\n      prs.map(async (pr) => {\n        const base = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${pr.number}`;\n\n        const [reviewsRes, reactionsRes] = await Promise.all([\n          fetch(`${base}/reviews?per_page=100`, { headers: ghHeaders }),\n          fetch(\n            `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${pr.number}/reactions`,\n            {\n              headers: {\n                ...ghHeaders,\n                Accept: \"application/vnd.github.squirrel-girl-preview+json\",\n              },\n            },\n          ),\n        ]);\n\n        const reviews: GitHubPRReview[] = reviewsRes.ok\n          ? ((await reviewsRes.json()) as GitHubPRReview[])\n          : [];\n\n        let reactionCount = 0;\n        if (reactionsRes.ok) {\n          const reactions = (await reactionsRes.json()) as GitHubPRReaction[];\n          reactionCount = reactions.length;\n        }\n\n        return {\n          number: pr.number,\n          title: pr.title,\n          state: pr.merged_at ? \"merged\" : pr.state,\n          author: pr.user?.login ?? \"unknown\",\n          url: pr.html_url,\n          createdAt: pr.created_at,\n          updatedAt: pr.updated_at,\n          comments: pr.comments,\n          reviews: reviews.length,\n          reactions: reactionCount,\n          labels: pr.labels.map((l) => l.name),\n          draft: pr.draft,\n        };\n      }),\n    );\n\n    return {\n      repository: `${owner}/${repo}`,\n      state: prState,\n      count: enriched.length,\n      pullRequests: enriched,\n    };\n  },\n});\n"
  },
  {
    "path": "examples/chat/lib/tools/hackernews.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Get top stories from Hacker News.\n * Uses the official HN Firebase API. Free, no auth required.\n * https://github.com/HackerNewsAPI/API\n */\nexport const getHackerNewsTop = tool({\n  description:\n    \"Get the current top stories from Hacker News, including title, score, author, URL, and comment count.\",\n  inputSchema: z.object({\n    count: z\n      .number()\n      .min(1)\n      .max(30)\n      .describe(\"Number of top stories to fetch (1-30)\"),\n  }),\n  execute: async ({ count }) => {\n    const topUrl =\n      \"https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty\";\n    const topRes = await fetch(topUrl);\n\n    if (!topRes.ok) {\n      return { error: \"Failed to fetch Hacker News top stories\" };\n    }\n\n    const topIds = (await topRes.json()) as number[];\n    const storyIds = topIds.slice(0, count);\n\n    const stories = await Promise.all(\n      storyIds.map(async (id) => {\n        const storyRes = await fetch(\n          `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`,\n        );\n        if (!storyRes.ok) return null;\n\n        const story = (await storyRes.json()) as {\n          id: number;\n          title: string;\n          url?: string;\n          score: number;\n          by: string;\n          time: number;\n          descendants?: number;\n          type: string;\n        };\n\n        return {\n          id: story.id,\n          title: story.title,\n          url: story.url ?? `https://news.ycombinator.com/item?id=${story.id}`,\n          score: story.score,\n          author: story.by,\n          comments: story.descendants ?? 0,\n          postedAt: new Date(story.time * 1000).toISOString(),\n          hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,\n        };\n      }),\n    );\n\n    return {\n      stories: stories.filter(Boolean),\n      fetchedAt: new Date().toISOString(),\n    };\n  },\n});\n"
  },
  {
    "path": "examples/chat/lib/tools/search.ts",
    "content": "import { tool, generateText } from \"ai\";\nimport { gateway } from \"@ai-sdk/gateway\";\nimport { z } from \"zod\";\n\n/**\n * Web search tool using Perplexity Sonar via AI Gateway.\n *\n * Perplexity Sonar models have built-in internet access and return\n * synthesized answers with citations. This is wrapped as a regular tool\n * (with an `execute` function) so that ToolLoopAgent can loop: it calls\n * the model, gets results, and feeds them back for the next step.\n */\nexport const webSearch = tool({\n  description:\n    \"Search the web for current information on any topic. Use this when the user asks about something not covered by the specialized tools (weather, crypto, GitHub, Hacker News). Returns a synthesized answer based on real-time web data.\",\n  inputSchema: z.object({\n    query: z\n      .string()\n      .describe(\n        \"The search query — be specific and include relevant context for better results\",\n      ),\n  }),\n  execute: async ({ query }) => {\n    try {\n      const { text } = await generateText({\n        model: gateway(\"perplexity/sonar\"),\n        prompt: query,\n      });\n      return { content: text };\n    } catch (error) {\n      return {\n        error: `Search failed: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      };\n    }\n  },\n});\n"
  },
  {
    "path": "examples/chat/lib/tools/weather.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Get current weather and 7-day forecast for a city using Open-Meteo API.\n * Free, no API key required.\n * https://open-meteo.com/\n */\nexport const getWeather = tool({\n  description:\n    \"Get current weather conditions and a 7-day forecast for a given city. Returns temperature, humidity, wind speed, weather conditions, and daily forecasts.\",\n  inputSchema: z.object({\n    city: z\n      .string()\n      .describe(\"City name (e.g., 'New York', 'London', 'Tokyo')\"),\n  }),\n  execute: async ({ city }) => {\n    // Step 1: Geocode the city name to coordinates\n    const geocodeUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`;\n    const geocodeRes = await fetch(geocodeUrl);\n\n    if (!geocodeRes.ok) {\n      return { error: `Failed to geocode city: ${city}` };\n    }\n\n    const geocodeData = (await geocodeRes.json()) as {\n      results?: Array<{\n        name: string;\n        country: string;\n        latitude: number;\n        longitude: number;\n        timezone: string;\n      }>;\n    };\n\n    if (!geocodeData.results || geocodeData.results.length === 0) {\n      return { error: `City not found: ${city}` };\n    }\n\n    const location = geocodeData.results[0]!;\n\n    // Step 2: Get weather data\n    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=${encodeURIComponent(location.timezone)}&forecast_days=7`;\n\n    const weatherRes = await fetch(weatherUrl);\n\n    if (!weatherRes.ok) {\n      return { error: \"Failed to fetch weather data\" };\n    }\n\n    const weather = (await weatherRes.json()) as {\n      current: {\n        temperature_2m: number;\n        relative_humidity_2m: number;\n        apparent_temperature: number;\n        weather_code: number;\n        wind_speed_10m: number;\n      };\n      daily: {\n        time: string[];\n        weather_code: number[];\n        temperature_2m_max: number[];\n        temperature_2m_min: number[];\n        precipitation_sum: number[];\n      };\n    };\n\n    const weatherDescription = describeWeatherCode(\n      weather.current.weather_code,\n    );\n\n    const forecast = weather.daily.time.map((date, i) => ({\n      date,\n      day: new Date(date + \"T12:00:00\").toLocaleDateString(\"en-US\", {\n        weekday: \"short\",\n      }),\n      high: Math.round(weather.daily.temperature_2m_max[i]!),\n      low: Math.round(weather.daily.temperature_2m_min[i]!),\n      condition: describeWeatherCode(weather.daily.weather_code[i]!),\n      precipitation: weather.daily.precipitation_sum[i]!,\n    }));\n\n    return {\n      city: location.name,\n      country: location.country,\n      current: {\n        temperature: Math.round(weather.current.temperature_2m),\n        feelsLike: Math.round(weather.current.apparent_temperature),\n        humidity: weather.current.relative_humidity_2m,\n        windSpeed: Math.round(weather.current.wind_speed_10m),\n        condition: weatherDescription,\n      },\n      forecast,\n    };\n  },\n});\n\nfunction describeWeatherCode(code: number): string {\n  const descriptions: Record<number, string> = {\n    0: \"Clear sky\",\n    1: \"Mainly clear\",\n    2: \"Partly cloudy\",\n    3: \"Overcast\",\n    45: \"Foggy\",\n    48: \"Depositing rime fog\",\n    51: \"Light drizzle\",\n    53: \"Moderate drizzle\",\n    55: \"Dense drizzle\",\n    61: \"Slight rain\",\n    63: \"Moderate rain\",\n    65: \"Heavy rain\",\n    71: \"Slight snow\",\n    73: \"Moderate snow\",\n    75: \"Heavy snow\",\n    77: \"Snow grains\",\n    80: \"Slight rain showers\",\n    81: \"Moderate rain showers\",\n    82: \"Violent rain showers\",\n    85: \"Slight snow showers\",\n    86: \"Heavy snow showers\",\n    95: \"Thunderstorm\",\n    96: \"Thunderstorm with slight hail\",\n    99: \"Thunderstorm with heavy hail\",\n  };\n  return descriptions[code] ?? \"Unknown\";\n}\n"
  },
  {
    "path": "examples/chat/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/chat/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/chat/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/chat/package.json",
    "content": "{\n  \"name\": \"example-chat\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless chat-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint --max-warnings 0\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.13\",\n    \"@ai-sdk/react\": \"^3.0.84\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/shadcn\": \"workspace:*\",\n    \"@react-three/drei\": \"^10.7.7\",\n    \"@react-three/fiber\": \"^9.5.0\",\n    \"@streamdown/code\": \"^1.0.2\",\n    \"ai\": \"^6.0.33\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.563.0\",\n    \"next\": \"16.1.6\",\n    \"next-themes\": \"^0.4.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"recharts\": \"^2.15.4\",\n    \"sonner\": \"^2.0.7\",\n    \"streamdown\": \"^2.2.0\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"three\": \"^0.182.0\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@types/three\": \"^0.182.0\",\n    \"eslint\": \"^9.39.1\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/chat/postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "examples/chat/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/dashboard/.env.example",
    "content": "# Database\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/json_render_dashboard_example\n\n# AI (optional - for UI generation)\nAI_GATEWAY_API_KEY=\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/dashboard/CHANGELOG.md",
    "content": "# example-dashboard\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/codegen@0.14.1\n  - @json-render/react@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/codegen@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/codegen@0.13.0\n  - @json-render/react@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/codegen@0.12.1\n  - @json-render/react@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/codegen@0.12.0\n  - @json-render/react@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/codegen@0.11.0\n  - @json-render/react@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n  - @json-render/codegen@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/core@0.9.1\n  - @json-render/codegen@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n  - @json-render/codegen@0.9.0\n"
  },
  {
    "path": "examples/dashboard/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { buildUserPrompt } from \"@json-render/core\";\nimport { dashboardCatalog } from \"@/lib/render/catalog\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 30;\n\nconst SYSTEM_PROMPT = dashboardCatalog.prompt();\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt, context } = await req.json();\n\n  const userPrompt = buildUserPrompt({\n    prompt,\n    state: context?.state,\n  });\n\n  const result = streamText({\n    model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    prompt: userPrompt,\n    temperature: 0.7,\n  });\n\n  return result.toTextStreamResponse();\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/accounts/[id]/route.ts",
    "content": "import { getAccount } from \"@/lib/db/store\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const account = await getAccount(id);\n\n  if (!account) {\n    return Response.json({ error: \"Account not found\" }, { status: 404 });\n  }\n\n  return Response.json(account);\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/accounts/route.ts",
    "content": "import { getAccounts } from \"@/lib/db/store\";\n\nexport async function GET() {\n  const accountList = await getAccounts();\n\n  return Response.json({\n    data: accountList,\n    total: accountList.length,\n    summary: {\n      totalBalance: accountList.reduce(\n        (sum, a) => sum + parseFloat(a.balance as string),\n        0,\n      ),\n      byType: {\n        bank: accountList\n          .filter((a) => a.type === \"bank\")\n          .reduce((sum, a) => sum + parseFloat(a.balance as string), 0),\n        credit_card: accountList\n          .filter((a) => a.type === \"credit_card\")\n          .reduce((sum, a) => sum + parseFloat(a.balance as string), 0),\n        cash: accountList\n          .filter((a) => a.type === \"cash\")\n          .reduce((sum, a) => sum + parseFloat(a.balance as string), 0),\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/customers/[id]/route.ts",
    "content": "import { getCustomer, updateCustomer, deleteCustomer } from \"@/lib/db/store\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const customer = await getCustomer(id);\n\n  if (!customer) {\n    return Response.json({ error: \"Customer not found\" }, { status: 404 });\n  }\n\n  return Response.json(customer);\n}\n\nexport async function PATCH(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const body = await req.json();\n  const customer = await updateCustomer(id, body);\n\n  if (!customer) {\n    return Response.json({ error: \"Customer not found\" }, { status: 404 });\n  }\n\n  return Response.json(customer);\n}\n\nexport async function DELETE(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const result = await deleteCustomer(id);\n\n  if (!result.success) {\n    return Response.json({ error: \"Customer not found\" }, { status: 404 });\n  }\n\n  return new Response(null, { status: 204 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/customers/route.ts",
    "content": "import { getCustomers, createCustomer } from \"@/lib/db/store\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const status = searchParams.get(\"status\") || undefined;\n  const search = searchParams.get(\"search\") || undefined;\n  const limit = searchParams.get(\"limit\")\n    ? parseInt(searchParams.get(\"limit\")!, 10)\n    : undefined;\n  const sort = (searchParams.get(\"sort\") as \"newest\" | \"oldest\") || undefined;\n\n  const customerList = await getCustomers({ status, search, limit, sort });\n\n  return Response.json({\n    data: customerList,\n    total: customerList.length,\n  });\n}\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n\n  const customer = await createCustomer({\n    name: body.name,\n    email: body.email,\n    phone: body.phone || undefined,\n  });\n\n  return Response.json(customer, { status: 201 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/expenses/[id]/approve/route.ts",
    "content": "import { approveExpense } from \"@/lib/db/store\";\n\nexport async function POST(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const result = await approveExpense(id);\n\n  if (result && \"error\" in result) {\n    return Response.json({ error: result.error }, { status: 400 });\n  }\n\n  return Response.json({\n    message: \"Expense approved\",\n    expense: result,\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/expenses/[id]/reject/route.ts",
    "content": "import { rejectExpense } from \"@/lib/db/store\";\n\nexport async function POST(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const result = await rejectExpense(id);\n\n  if (result && \"error\" in result) {\n    return Response.json({ error: result.error }, { status: 400 });\n  }\n\n  return Response.json({\n    message: \"Expense rejected\",\n    expense: result,\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/expenses/[id]/route.ts",
    "content": "import { getExpense, updateExpense, deleteExpense } from \"@/lib/db/store\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const expense = await getExpense(id);\n\n  if (!expense) {\n    return Response.json({ error: \"Expense not found\" }, { status: 404 });\n  }\n\n  return Response.json(expense);\n}\n\nexport async function PATCH(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const body = await req.json();\n  const expense = await updateExpense(id, body);\n\n  if (!expense) {\n    return Response.json({ error: \"Expense not found\" }, { status: 404 });\n  }\n\n  return Response.json(expense);\n}\n\nexport async function DELETE(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const deleted = await deleteExpense(id);\n\n  if (!deleted) {\n    return Response.json({ error: \"Expense not found\" }, { status: 404 });\n  }\n\n  return new Response(null, { status: 204 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/expenses/route.ts",
    "content": "import { getExpenses, createExpense } from \"@/lib/db/store\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const status = searchParams.get(\"status\") || undefined;\n  const category = searchParams.get(\"category\") || undefined;\n\n  const expenseList = await getExpenses({ status, category });\n\n  return Response.json({\n    data: expenseList,\n    total: expenseList.length,\n    summary: {\n      totalAmount: expenseList.reduce(\n        (sum, e) => sum + parseFloat(e.amount as string),\n        0,\n      ),\n      byStatus: {\n        pending: expenseList.filter((e) => e.status === \"pending\").length,\n        approved: expenseList.filter((e) => e.status === \"approved\").length,\n        rejected: expenseList.filter((e) => e.status === \"rejected\").length,\n      },\n    },\n  });\n}\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n\n  const expense = await createExpense({\n    vendor: body.vendor,\n    category: body.category,\n    amount: body.amount,\n    date: body.date,\n    description: body.description,\n  });\n\n  return Response.json(expense, { status: 201 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/invoices/[id]/mark-paid/route.ts",
    "content": "import { markInvoicePaid } from \"@/lib/db/store\";\n\nexport async function POST(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const result = await markInvoicePaid(id);\n\n  if (result && \"error\" in result) {\n    return Response.json({ error: result.error }, { status: 400 });\n  }\n\n  return Response.json({\n    message: \"Invoice marked as paid\",\n    invoice: result,\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/invoices/[id]/route.ts",
    "content": "import { getInvoice, updateInvoice, deleteInvoice } from \"@/lib/db/store\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const invoice = await getInvoice(id);\n\n  if (!invoice) {\n    return Response.json({ error: \"Invoice not found\" }, { status: 404 });\n  }\n\n  return Response.json(invoice);\n}\n\nexport async function PATCH(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const body = await req.json();\n  const invoice = await updateInvoice(id, body);\n\n  if (!invoice) {\n    return Response.json({ error: \"Invoice not found\" }, { status: 404 });\n  }\n\n  return Response.json(invoice);\n}\n\nexport async function DELETE(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const deleted = await deleteInvoice(id);\n\n  if (!deleted) {\n    return Response.json({ error: \"Invoice not found\" }, { status: 404 });\n  }\n\n  return new Response(null, { status: 204 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/invoices/[id]/send/route.ts",
    "content": "import { sendInvoice } from \"@/lib/db/store\";\n\nexport async function POST(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const result = await sendInvoice(id);\n\n  if (result && \"error\" in result) {\n    return Response.json({ error: result.error }, { status: 400 });\n  }\n\n  return Response.json({\n    message: \"Invoice sent successfully\",\n    invoice: result,\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/invoices/route.ts",
    "content": "import { getInvoices, createInvoice } from \"@/lib/db/store\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const status = searchParams.get(\"status\") || undefined;\n  const customerId = searchParams.get(\"customerId\") || undefined;\n\n  const invoiceList = await getInvoices({ status, customerId });\n\n  return Response.json({\n    data: invoiceList,\n    total: invoiceList.length,\n    summary: {\n      totalAmount: invoiceList.reduce(\n        (sum, i) => sum + parseFloat(i.amount as string),\n        0,\n      ),\n      byStatus: {\n        draft: invoiceList.filter((i) => i.status === \"draft\").length,\n        sent: invoiceList.filter((i) => i.status === \"sent\").length,\n        paid: invoiceList.filter((i) => i.status === \"paid\").length,\n        overdue: invoiceList.filter((i) => i.status === \"overdue\").length,\n      },\n    },\n  });\n}\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n\n  const invoice = await createInvoice({\n    customerId: body.customerId,\n    dueDate: body.dueDate,\n    items: body.items,\n  });\n\n  if (!invoice) {\n    return Response.json({ error: \"Customer not found\" }, { status: 400 });\n  }\n\n  return Response.json(invoice, { status: 201 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/reports/export/route.ts",
    "content": "import {\n  getDashboardSummary,\n  getInvoices,\n  getExpenses,\n  getCustomers,\n} from \"@/lib/db/store\";\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n  const { format = \"json\", reportType = \"summary\" } = body;\n\n  let data: unknown;\n\n  switch (reportType) {\n    case \"summary\":\n      data = await getDashboardSummary();\n      break;\n    case \"invoices\":\n      data = await getInvoices();\n      break;\n    case \"expenses\":\n      data = await getExpenses();\n      break;\n    case \"customers\":\n      data = await getCustomers();\n      break;\n    default:\n      data = await getDashboardSummary();\n  }\n\n  if (format === \"csv\") {\n    return Response.json({\n      message: \"CSV export initiated\",\n      downloadUrl: `/api/v1/reports/download/${reportType}.csv`,\n      format: \"csv\",\n    });\n  }\n\n  return Response.json({\n    message: \"Report generated\",\n    reportType,\n    generatedAt: new Date().toISOString(),\n    data,\n  });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/reports/profit-loss/route.ts",
    "content": "import { getProfitLossReport } from \"@/lib/db/store\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const startDate = searchParams.get(\"startDate\") || undefined;\n  const endDate = searchParams.get(\"endDate\") || undefined;\n\n  const report = await getProfitLossReport(startDate, endDate);\n  return Response.json(report);\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/reset/route.ts",
    "content": "import { resetDatabase } from \"@/lib/db/store\";\n\nexport async function POST() {\n  const result = await resetDatabase();\n  return Response.json(result);\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/widgets/[id]/route.ts",
    "content": "import { getWidget, updateWidget, deleteWidget } from \"@/lib/db/store\";\n\nexport async function GET(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const widget = await getWidget(id);\n\n  if (!widget) {\n    return Response.json({ error: \"Widget not found\" }, { status: 404 });\n  }\n\n  return Response.json(widget);\n}\n\nexport async function PATCH(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const body = await req.json();\n  const widget = await updateWidget(id, body);\n\n  if (!widget) {\n    return Response.json({ error: \"Widget not found\" }, { status: 404 });\n  }\n\n  return Response.json(widget);\n}\n\nexport async function DELETE(\n  req: Request,\n  { params }: { params: Promise<{ id: string }> },\n) {\n  const { id } = await params;\n  const deleted = await deleteWidget(id);\n\n  if (!deleted) {\n    return Response.json({ error: \"Widget not found\" }, { status: 404 });\n  }\n\n  return new Response(null, { status: 204 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/widgets/reorder/route.ts",
    "content": "import { reorderWidgets } from \"@/lib/db/store\";\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n\n  if (!body.orderedIds || !Array.isArray(body.orderedIds)) {\n    return Response.json(\n      { error: \"orderedIds array is required\" },\n      { status: 400 },\n    );\n  }\n\n  await reorderWidgets(body.orderedIds);\n  return Response.json({ success: true });\n}\n"
  },
  {
    "path": "examples/dashboard/app/api/v1/widgets/route.ts",
    "content": "import { getWidgets, createWidget } from \"@/lib/db/store\";\n\nexport async function GET() {\n  try {\n    const widgetList = await getWidgets();\n    return Response.json({ data: widgetList, total: widgetList.length });\n  } catch {\n    return Response.json({ data: [], total: 0 });\n  }\n}\n\nexport async function POST(req: Request) {\n  const body = await req.json();\n\n  if (!body.prompt || !body.spec) {\n    return Response.json(\n      { error: \"prompt and spec are required\" },\n      { status: 400 },\n    );\n  }\n\n  const widget = await createWidget({\n    prompt: body.prompt,\n    spec: body.spec,\n  });\n\n  return Response.json(widget, { status: 201 });\n}\n"
  },
  {
    "path": "examples/dashboard/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n  --color-background: #000;\n  --color-foreground: #fafafa;\n  --color-card: #0a0a0a;\n  --color-card-foreground: #fafafa;\n  --color-primary: #fafafa;\n  --color-primary-foreground: #000;\n  --color-secondary: #262626;\n  --color-secondary-foreground: #fafafa;\n  --color-muted: #262626;\n  --color-muted-foreground: #a3a3a3;\n  --color-accent: #262626;\n  --color-accent-foreground: #fafafa;\n  --color-destructive: #dc2626;\n  --color-destructive-foreground: #fafafa;\n  --color-border: #262626;\n  --color-input: #262626;\n  --color-ring: #fafafa;\n  --radius: 0.5rem;\n}\n\n* {\n  box-sizing: border-box;\n  border-color: var(--color-border);\n}\n\nbody {\n  background-color: var(--color-background);\n  color: var(--color-foreground);\n  font-family: var(--font-geist-sans), system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n\ncode, pre, .font-mono {\n  font-family: var(--font-geist-mono), ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --radius-3xl: calc(var(--radius) + 12px);\n  --radius-4xl: calc(var(--radius) + 16px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbutton {\n  cursor: pointer;\n}\n\n/* Shiki dual theme support */\n.shiki,\n.shiki span {\n  color: var(--shiki-light) !important;\n  background-color: var(--shiki-light-bg) !important;\n}\n\n.dark .shiki,\n.dark .shiki span {\n  color: var(--shiki-dark) !important;\n  background-color: var(--shiki-dark-bg) !important;\n}\n"
  },
  {
    "path": "examples/dashboard/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport { Toaster } from \"sonner\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport \"./globals.css\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n});\n\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n});\n\nexport const metadata: Metadata = {\n  title: \"Dashboard | json-render\",\n  description: \"AI-generated dashboard widgets with guardrails\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}\n      >\n        <ThemeProvider>\n          {children}\n          <Toaster />\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useEffect } from \"react\";\nimport {\n  DndContext,\n  closestCenter,\n  KeyboardSensor,\n  PointerSensor,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n  arrayMove,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  rectSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { Widget } from \"@/components/widget\";\nimport { Header } from \"@/components/header\";\nimport { AddWidgetCard } from \"@/components/add-widget-card\";\nimport { SortableWidget, type SavedWidget } from \"@/components/sortable-widget\";\n\nfunction DashboardContent() {\n  const [savedWidgets, setSavedWidgets] = useState<SavedWidget[]>([]);\n  const [newWidgetCount, setNewWidgetCount] = useState(0);\n  const [isLoading, setIsLoading] = useState(true);\n\n  const sensors = useSensors(\n    useSensor(PointerSensor),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  );\n\n  // Load saved widgets on mount\n  useEffect(() => {\n    async function loadWidgets() {\n      try {\n        const res = await fetch(\"/api/v1/widgets\");\n        if (!res.ok) throw new Error(`HTTP ${res.status}`);\n        const data = await res.json();\n        setSavedWidgets(data.data || []);\n      } catch (err) {\n        console.error(\"Failed to load widgets:\", err);\n      } finally {\n        setIsLoading(false);\n      }\n    }\n    loadWidgets();\n  }, []);\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const handleWidgetSaved = useCallback((_id: string) => {\n    // Reload saved widgets to get the new one\n    fetch(\"/api/v1/widgets\")\n      .then((res) => (res.ok ? res.json() : { data: [] }))\n      .then((data) => {\n        setSavedWidgets(data.data || []);\n        // Remove the new widget slot since it's now saved\n        setNewWidgetCount((prev) => Math.max(0, prev - 1));\n      });\n  }, []);\n\n  const handleAddWidget = useCallback(() => {\n    setNewWidgetCount((prev) => prev + 1);\n  }, []);\n\n  const handleClearWidget = useCallback(async (id?: string) => {\n    if (id) {\n      // Delete from database\n      await fetch(`/api/v1/widgets/${id}`, { method: \"DELETE\" });\n      setSavedWidgets((prev) => prev.filter((w) => w.id !== id));\n    } else {\n      // Just remove the new widget slot\n      setNewWidgetCount((prev) => Math.max(0, prev - 1));\n    }\n  }, []);\n\n  const handleDragEnd = useCallback(async (event: DragEndEvent) => {\n    const { active, over } = event;\n\n    if (over && active.id !== over.id) {\n      setSavedWidgets((items) => {\n        const oldIndex = items.findIndex((item) => item.id === active.id);\n        const newIndex = items.findIndex((item) => item.id === over.id);\n        const newItems = arrayMove(items, oldIndex, newIndex);\n\n        // Persist the new order to the server\n        fetch(\"/api/v1/widgets/reorder\", {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ orderedIds: newItems.map((w) => w.id) }),\n        }).catch((err) => console.error(\"Failed to save widget order:\", err));\n\n        return newItems;\n      });\n    }\n  }, []);\n\n  if (isLoading) {\n    return (\n      <div className=\"min-h-screen flex items-center justify-center\">\n        <p className=\"text-muted-foreground\">Loading...</p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"min-h-screen flex flex-col\">\n      <Header />\n      <main className=\"flex-1 p-6\">\n        <DndContext\n          sensors={sensors}\n          collisionDetection={closestCenter}\n          onDragEnd={handleDragEnd}\n        >\n          <SortableContext\n            items={savedWidgets.map((w) => w.id)}\n            strategy={rectSortingStrategy}\n          >\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\n              {/* Saved widgets */}\n              {savedWidgets.map((widget) => (\n                <SortableWidget\n                  key={widget.id}\n                  widget={widget}\n                  onDeleted={() => handleClearWidget(widget.id)}\n                />\n              ))}\n\n              {/* New empty widgets */}\n              {Array.from({ length: newWidgetCount }).map((_, i) => (\n                <Widget\n                  key={`new-${i}`}\n                  onSaved={handleWidgetSaved}\n                  onDeleted={() => handleClearWidget()}\n                />\n              ))}\n\n              {/* Add widget card */}\n              <AddWidgetCard onClick={handleAddWidget} />\n            </div>\n          </SortableContext>\n        </DndContext>\n      </main>\n    </div>\n  );\n}\n\nexport default function DashboardPage() {\n  return <DashboardContent />;\n}\n"
  },
  {
    "path": "examples/dashboard/components/add-widget-card.tsx",
    "content": "\"use client\";\n\nimport { Plus } from \"lucide-react\";\n\ninterface AddWidgetCardProps {\n  onClick: () => void;\n}\n\nexport function AddWidgetCard({ onClick }: AddWidgetCardProps) {\n  return (\n    <button\n      onClick={onClick}\n      className=\"aspect-[4/3] flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/30 transition-colors cursor-pointer\"\n    >\n      <Plus className=\"h-8 w-8 text-muted-foreground/50\" />\n      <span className=\"mt-2 text-sm text-muted-foreground/50\">Add Widget</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/code-highlight.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { codeToHtml } from \"shiki\";\n\nconst vercelDarkTheme = {\n  name: \"vercel-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#666666\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#7928CA\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#FF0080\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#888888\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#EDEDED\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n  ],\n};\n\nconst vercelLightTheme = {\n  name: \"vercel-light\",\n  type: \"light\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#171717\",\n  },\n  settings: [\n    {\n      scope: [\"comment\", \"punctuation.definition.comment\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\"string\", \"string.quoted\", \"string.template\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"keyword\", \"storage.type\", \"storage.modifier\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"keyword.operator\", \"keyword.control\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"entity.name.function\", \"support.function\", \"meta.function-call\"],\n      settings: { foreground: \"#6e56cf\" },\n    },\n    {\n      scope: [\"variable\", \"variable.other\", \"variable.parameter\"],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.name.tag\", \"support.class.component\", \"entity.name.type\"],\n      settings: { foreground: \"#d6409f\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#6b7280\" },\n    },\n    {\n      scope: [\n        \"support.type.property-name\",\n        \"entity.name.tag.json\",\n        \"meta.object-literal.key\",\n      ],\n      settings: { foreground: \"#171717\" },\n    },\n    {\n      scope: [\"entity.other.attribute-name\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n    {\n      scope: [\"support.type.primitive\", \"entity.name.type.primitive\"],\n      settings: { foreground: \"#067a6e\" },\n    },\n  ],\n};\n\ninterface CodeHighlightProps {\n  code: string;\n  language?: string;\n}\n\nexport function CodeHighlight({ code, language = \"json\" }: CodeHighlightProps) {\n  const [html, setHtml] = useState<string>(\"\");\n\n  useEffect(() => {\n    codeToHtml(code, {\n      lang: language,\n      themes: {\n        light: vercelLightTheme,\n        dark: vercelDarkTheme,\n      },\n      defaultColor: false,\n    }).then(setHtml);\n  }, [code, language]);\n\n  if (!html) {\n    return (\n      <pre className=\"text-xs font-mono whitespace-pre-wrap break-all\">\n        {code}\n      </pre>\n    );\n  }\n\n  return (\n    <div\n      className=\"text-xs [&_pre]:!bg-transparent [&_pre]:!p-0 [&_code]:!bg-transparent [&_.shiki]:!bg-transparent\"\n      dangerouslySetInnerHTML={{ __html: html }}\n    />\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/header.tsx",
    "content": "\"use client\";\n\nimport { ThemeToggle } from \"./theme-toggle\";\n\nexport function Header() {\n  return (\n    <header className=\"sticky top-0 z-50 bg-background\">\n      <div className=\"flex h-14 items-center justify-between px-4 gap-6\">\n        <div className=\"flex items-center gap-2\">\n          <a href=\"https://vercel.com\" title=\"Made with love by Vercel\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"18\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"18\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M8 1L16 15H0L8 1Z\"\n                fill=\"currentColor\"\n              ></path>\n            </svg>\n          </a>\n          <span className=\"text-(--ds-gray-500)\">\n            <svg\n              data-testid=\"geist-icon\"\n              height=\"16\"\n              strokeLinejoin=\"round\"\n              viewBox=\"0 0 16 16\"\n              width=\"16\"\n              style={{ color: \"currentcolor\" }}\n            >\n              <path\n                fillRule=\"evenodd\"\n                clipRule=\"evenodd\"\n                d=\"M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z\"\n                fill=\"currentColor\"\n              ></path>\n            </svg>\n          </span>\n          <span className=\"font-medium tracking-tight text-lg\">\n            Dashboard Example\n          </span>\n        </div>\n        <nav className=\"flex items-center gap-4\">\n          <a\n            href=\"https://json-render.vercel.app\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"text-sm text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            Docs\n          </a>\n          <a\n            href=\"https://github.com/vercel-labs/json-render\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <svg\n              viewBox=\"0 0 16 16\"\n              className=\"h-4 w-4\"\n              fill=\"currentColor\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n            </svg>\n            <span className=\"hidden sm:inline\">GitHub</span>\n          </a>\n          <ThemeToggle />\n        </nav>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/sortable-widget.tsx",
    "content": "\"use client\";\n\nimport { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport type { Spec } from \"@json-render/react\";\nimport { Widget } from \"./widget\";\n\nexport interface SavedWidget {\n  id: string;\n  prompt: string;\n  spec: Spec;\n}\n\ninterface SortableWidgetProps {\n  widget: SavedWidget;\n  onDeleted: () => void;\n}\n\nexport function SortableWidget({ widget, onDeleted }: SortableWidgetProps) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: widget.id });\n\n  const style = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n    opacity: isDragging ? 0.5 : 1,\n  };\n\n  return (\n    <div ref={setNodeRef} style={style}>\n      <Widget\n        id={widget.id}\n        initialPrompt={widget.prompt}\n        initialSpec={widget.spec}\n        onDeleted={onDeleted}\n        dragHandleProps={{ ...attributes, ...listeners }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\n\nexport function ThemeProvider({ children }: { children: React.ReactNode }) {\n  return (\n    <NextThemesProvider\n      attribute=\"class\"\n      defaultTheme=\"system\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      {children}\n    </NextThemesProvider>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/theme-toggle.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useState } from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\n\nexport function ThemeToggle() {\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  if (!mounted) {\n    return (\n      <button className=\"p-2 rounded-md border border-border bg-card\">\n        <Sun className=\"h-4 w-4\" />\n      </button>\n    );\n  }\n\n  return (\n    <button\n      onClick={() => setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n      className=\"p-2 rounded-md border border-border bg-card hover:bg-accent transition-colors\"\n      aria-label=\"Toggle theme\"\n    >\n      {theme === \"dark\" ? (\n        <Sun className=\"h-4 w-4\" />\n      ) : (\n        <Moon className=\"h-4 w-4\" />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components/ui/accordion.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n      {...props}\n    >\n      <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "examples/dashboard/components/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "examples/dashboard/components/ui/animated-border.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface AnimatedBorderProps {\n  className?: string;\n}\n\nexport const AnimatedBorder = ({ className }: AnimatedBorderProps) => {\n  return (\n    <>\n      <style jsx>{`\n        @property --angle {\n          syntax: \"<angle>\";\n          initial-value: 0deg;\n          inherits: false;\n        }\n\n        @keyframes border-rotate {\n          from {\n            --angle: 0deg;\n          }\n          to {\n            --angle: 360deg;\n          }\n        }\n\n        .animate-border-mask {\n          animation: border-rotate 2s linear infinite;\n          mask-image: conic-gradient(\n            from var(--angle),\n            transparent 70%,\n            black 90%,\n            transparent 100%\n          );\n        }\n      `}</style>\n      <div\n        className={cn(\n          \"pointer-events-none absolute inset-0 rounded-[inherit] animate-border-mask z-10\",\n          className,\n        )}\n      >\n        <div\n          className=\"absolute inset-0 rounded-[inherit] border border-blue-400\"\n          style={{\n            boxShadow: \"0 0 6px 1px #3b82f6, inset 0 0 2px 0 #3b82f6\",\n          }}\n        />\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Avatar({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root> & {\n  size?: \"default\" | \"sm\" | \"lg\";\n}) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      data-size={size}\n      className={cn(\n        \"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarBadge({ className, ...props }: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"avatar-badge\"\n      className={cn(\n        \"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none\",\n        \"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden\",\n        \"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2\",\n        \"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"avatar-group\"\n      className={cn(\n        \"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarGroupCount({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"avatar-group-count\"\n      className={cn(\n        \"bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Avatar,\n  AvatarImage,\n  AvatarFallback,\n  AvatarBadge,\n  AvatarGroup,\n  AvatarGroupCount,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        ghost: \"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 [a&]:hover:underline\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      data-variant={variant}\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "examples/dashboard/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "examples/dashboard/components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\n        \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardAction,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/chart.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  );\n};\n\ntype ChartContextProps = {\n  config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  config: ChartConfig;\n  children: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"children\"];\n}) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color,\n  );\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color;\n    return color ? `  --color-${key}: ${color};` : null;\n  })\n  .join(\"\\n\")}\n}\n`,\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n  }) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ]);\n\n  if (!active || !payload?.length) {\n    return null;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n            const indicatorColor = color || item.payload.fill || item.color;\n\n            return (\n              <div\n                key={item.dataKey}\n                className={cn(\n                  \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                  indicator === \"dot\" && \"items-center\",\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            },\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\",\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value && (\n                        <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n}: React.ComponentProps<\"div\"> &\n  Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n    hideIcon?: boolean;\n    nameKey?: string;\n  }) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className,\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n          return (\n            <div\n              key={item.value}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\",\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string,\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[\n      key as keyof typeof payloadPayload\n    ] as string;\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config];\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"grid place-content-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox };\n"
  },
  {
    "path": "examples/dashboard/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogFooter({\n  className,\n  showCloseButton = false,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {showCloseButton && (\n        <DialogPrimitive.Close asChild>\n          <Button variant=\"outline\">Close</Button>\n        </DialogPrimitive.Close>\n      )}\n    </div>\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/drawer.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Drawer({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n  return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />;\n}\n\nfunction DrawerTrigger({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return (\n    <DrawerPrimitive.Overlay\n      data-slot=\"drawer-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  return (\n    <DrawerPortal data-slot=\"drawer-portal\">\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        data-slot=\"drawer-content\"\n        className={cn(\n          \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n          \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n          \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n          \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n        {children}\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  );\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\n        \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return (\n    <DrawerPrimitive.Title\n      data-slot=\"drawer-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return (\n    <DrawerPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "examples/dashboard/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "examples/dashboard/components/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MoreHorizontalIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants, type Button } from \"@/components/ui/button\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n  return (\n    <nav\n      role=\"navigation\"\n      aria-label=\"pagination\"\n      data-slot=\"pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationContent({\n  className,\n  ...props\n}: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"pagination-content\"\n      className={cn(\"flex flex-row items-center gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n  return <li data-slot=\"pagination-item\" {...props} />;\n}\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nfunction PaginationLink({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) {\n  return (\n    <a\n      aria-current={isActive ? \"page\" : undefined}\n      data-slot=\"pagination-link\"\n      data-active={isActive}\n      className={cn(\n        buttonVariants({\n          variant: isActive ? \"outline\" : \"ghost\",\n          size,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationPrevious({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to previous page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pl-2.5\", className)}\n      {...props}\n    >\n      <ChevronLeftIcon />\n      <span className=\"hidden sm:block\">Previous</span>\n    </PaginationLink>\n  );\n}\n\nfunction PaginationNext({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to next page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pr-2.5\", className)}\n      {...props}\n    >\n      <span className=\"hidden sm:block\">Next</span>\n      <ChevronRightIcon />\n    </PaginationLink>\n  );\n}\n\nfunction PaginationEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"pagination-ellipsis\"\n      className={cn(\"flex size-9 items-center justify-center\", className)}\n      {...props}\n    >\n      <MoreHorizontalIcon className=\"size-4\" />\n      <span className=\"sr-only\">More pages</span>\n    </span>\n  );\n}\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"popover-header\"\n      className={cn(\"flex flex-col gap-1 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  return (\n    <div\n      data-slot=\"popover-title\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"popover-description\"\n      className={cn(\"text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverAnchor,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverDescription,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Progress({\n  className,\n  value,\n  ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\n        \"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator\n        data-slot=\"progress-indicator\"\n        className=\"bg-primary h-full w-full flex-1 transition-all\"\n        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n      />\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport { Progress };\n"
  },
  {
    "path": "examples/dashboard/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CircleIcon } from \"lucide-react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "examples/dashboard/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"item-aligned\",\n  align = \"center\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        align={align}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        data-slot=\"select-item-indicator\"\n        className=\"absolute right-2 flex size-3.5 items-center justify-center\"\n      >\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "examples/dashboard/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "examples/dashboard/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Switch({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      data-size={size}\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "examples/dashboard/components/ui/table.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "examples/dashboard/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst tabsListVariants = cva(\n  \"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction TabsList({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n  VariantProps<typeof tabsListVariants>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground\",\n        \"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n"
  },
  {
    "path": "examples/dashboard/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "examples/dashboard/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />;\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "examples/dashboard/components/widget.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useCallback, useRef, useEffect } from \"react\";\nimport { toast } from \"sonner\";\nimport {\n  Check,\n  Code,\n  Copy,\n  GripVertical,\n  Pencil,\n  Trash2,\n  X,\n} from \"lucide-react\";\nimport { useUIStream, type Spec } from \"@json-render/react\";\nimport { DashboardRenderer } from \"@/lib/render/renderer\";\nimport { executeAction } from \"@/lib/render/registry\";\nimport { CodeHighlight } from \"@/components/code-highlight\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { AnimatedBorder } from \"@/components/ui/animated-border\";\n\nconst suggestions = [\n  \"Customer list with delete\",\n  \"Create customer form\",\n  \"Invoice summary\",\n  \"Revenue metrics\",\n];\n\ninterface WidgetProps {\n  id?: string;\n  initialPrompt?: string;\n  initialSpec?: Spec;\n  onGenerated?: () => void;\n  onCleared?: () => void;\n  onDeleted?: () => void;\n  onSaved?: (id: string) => void;\n  dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;\n}\n\nexport function Widget({\n  id: initialId,\n  initialPrompt,\n  initialSpec,\n  onGenerated,\n  onCleared,\n  onDeleted,\n  onSaved,\n  dragHandleProps,\n}: WidgetProps): React.ReactElement {\n  const [prompt, setPrompt] = useState(initialPrompt || \"\");\n  const [state, setState] = useState<Record<string, unknown>>({});\n  const [widgetId, setWidgetId] = useState<string | undefined>(initialId);\n  const [isEditing, setIsEditing] = useState(false);\n  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);\n  const [showCode, setShowCode] = useState(false);\n  const [copied, setCopied] = useState(false);\n  const [editPrompt, setEditPrompt] = useState(\"\");\n  const promptRef = useRef(prompt);\n  const stateRef = useRef(state);\n  const editInputRef = useRef<HTMLInputElement>(null);\n  const promptInputRef = useRef<HTMLInputElement>(null);\n\n  // Keep stateRef in sync\n  useEffect(() => {\n    stateRef.current = state;\n  }, [state]);\n\n  // Auto-focus prompt input for new widgets\n  useEffect(() => {\n    if (!initialSpec && promptInputRef.current) {\n      promptInputRef.current.focus();\n    }\n  }, [initialSpec]);\n\n  const { spec, isStreaming, error, send, clear } = useUIStream({\n    api: \"/api/generate\",\n    onError: (err) => console.error(\"Widget generation error:\", err),\n    onComplete: async (completedSpec) => {\n      // Save to database when generation completes\n      const currentPrompt = promptRef.current;\n      if (completedSpec && currentPrompt) {\n        try {\n          if (widgetId) {\n            // Update existing widget\n            await fetch(`/api/v1/widgets/${widgetId}`, {\n              method: \"PATCH\",\n              headers: { \"Content-Type\": \"application/json\" },\n              body: JSON.stringify({\n                prompt: currentPrompt,\n                spec: completedSpec,\n              }),\n            });\n          } else {\n            // Create new widget\n            const res = await fetch(\"/api/v1/widgets\", {\n              method: \"POST\",\n              headers: { \"Content-Type\": \"application/json\" },\n              body: JSON.stringify({\n                prompt: currentPrompt,\n                spec: completedSpec,\n              }),\n            });\n            const saved = await res.json();\n            setWidgetId(saved.id);\n            onSaved?.(saved.id);\n          }\n        } catch (err) {\n          console.error(\"Failed to save widget:\", err);\n        }\n      }\n    },\n  });\n\n  // Keep promptRef in sync\n  useEffect(() => {\n    promptRef.current = prompt;\n  }, [prompt]);\n\n  const handleGenerate = useCallback(\n    async (text?: string) => {\n      const p = text || prompt;\n      if (!p.trim()) return;\n      if (text) setPrompt(text);\n      await send(p, { state });\n      onGenerated?.();\n    },\n    [prompt, send, state, onGenerated],\n  );\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  const handleClear = useCallback(() => {\n    clear();\n    setPrompt(\"\");\n    setState({});\n    onCleared?.();\n  }, [clear, onCleared]);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleGenerate();\n      }\n    },\n    [handleGenerate],\n  );\n\n  const handleEdit = useCallback(() => {\n    setEditPrompt(\"\");\n    setIsEditing(true);\n    setTimeout(() => editInputRef.current?.focus(), 0);\n  }, []);\n\n  const handleEditSubmit = useCallback(async () => {\n    if (editPrompt.trim()) {\n      setIsEditing(false);\n      // Pass the current spec so AI can modify it instead of replacing\n      const existingSpec = spec || initialSpec;\n      await send(editPrompt, { state, previousSpec: existingSpec });\n      onGenerated?.();\n    }\n  }, [editPrompt, send, state, spec, initialSpec, onGenerated]);\n\n  const handleEditKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleEditSubmit();\n      } else if (e.key === \"Escape\") {\n        setIsEditing(false);\n      }\n    },\n    [handleEditSubmit],\n  );\n\n  const handleDelete = useCallback(async () => {\n    if (widgetId) {\n      try {\n        await fetch(`/api/v1/widgets/${widgetId}`, { method: \"DELETE\" });\n        toast.success(\"Widget deleted\");\n      } catch (err) {\n        console.error(\"Failed to delete widget:\", err);\n        toast.error(\"Failed to delete widget\");\n      }\n    }\n    setShowDeleteConfirm(false);\n    onDeleted?.();\n  }, [widgetId, onDeleted]);\n\n  const handleStateChange = useCallback(\n    (changes: Array<{ path: string; value: unknown }>) => {\n      setState((prev) => {\n        const next = { ...prev };\n        for (const { path, value } of changes) {\n          const parts = path.split(\"/\");\n          let current: Record<string, unknown> = next;\n          for (let i = 0; i < parts.length - 1; i++) {\n            const part = parts[i]!;\n            if (!(part in current) || typeof current[part] !== \"object\") {\n              current[part] = {};\n            }\n            current = current[part] as Record<string, unknown>;\n          }\n          const lastPart = parts[parts.length - 1]!;\n          current[lastPart] = value;\n        }\n        return next;\n      });\n    },\n    [],\n  );\n\n  // Use spec from stream, or initial spec for saved widgets\n  const currentSpec = spec || initialSpec;\n\n  // Auto-run initial actions when spec loads (for saved widgets)\n  useEffect(() => {\n    if (!currentSpec || isStreaming) return;\n\n    // Check for initialActions in spec metadata\n    const specWithMeta = currentSpec as Spec & {\n      initialActions?: Array<{\n        action: string;\n        params?: Record<string, unknown>;\n      }>;\n    };\n    if (specWithMeta.initialActions) {\n      specWithMeta.initialActions.forEach(({ action, params }) => {\n        executeAction(action, params, setState);\n      });\n      return;\n    }\n\n    // Auto-detect: find Tables/Charts and run their data-fetching actions\n    const elements = currentSpec.elements || {};\n    const dataComponents = [\"Table\", \"BarChart\", \"LineChart\"];\n    const actionsRun = new Set<string>();\n\n    for (const el of Object.values(elements)) {\n      const element = el as { type: string; props?: Record<string, unknown> };\n      if (dataComponents.includes(element.type) && element.props?.statePath) {\n        const statePath = element.props.statePath as string;\n        // Extract resource name (e.g., \"customers\" from \"customers.data\")\n        const resource = statePath.split(\".\")[0];\n        if (!resource || actionsRun.has(resource)) continue;\n        // Find a button that loads this data\n        for (const btnEl of Object.values(elements)) {\n          const btn = btnEl as {\n            type: string;\n            props?: Record<string, unknown>;\n          };\n          if (btn.type === \"Button\" && btn.props?.action) {\n            const action = btn.props.action as string;\n            if (\n              action.toLowerCase().includes(resource) &&\n              (action.startsWith(\"view\") ||\n                action.startsWith(\"refresh\") ||\n                action.startsWith(\"load\"))\n            ) {\n              // Run this action with its params\n              executeAction(\n                action,\n                btn.props.actionParams as Record<string, unknown>,\n                setState,\n              );\n              actionsRun.add(resource);\n              break;\n            }\n          }\n        }\n      }\n    }\n  }, [currentSpec, isStreaming]);\n  const hasContent =\n    currentSpec && Object.keys(currentSpec.elements).length > 0;\n\n  // Title derived from prompt\n  const title = prompt.trim() || \"New Widget\";\n\n  const handleCopyCode = async () => {\n    if (currentSpec) {\n      await navigator.clipboard.writeText(JSON.stringify(currentSpec, null, 2));\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    }\n  };\n\n  return (\n    <Card className=\"group relative flex flex-col aspect-[4/3] py-0 gap-0\">\n      {isStreaming && <AnimatedBorder />}\n      {/* Title bar */}\n      <div className=\"px-3 py-2 border-b flex items-center justify-between bg-muted/30\">\n        <div className=\"flex items-center gap-1 flex-1 min-w-0\">\n          {dragHandleProps && (\n            <button\n              {...dragHandleProps}\n              className=\"h-6 w-6 p-0 flex items-center justify-center text-muted-foreground hover:text-foreground cursor-grab active:cursor-grabbing touch-none\"\n              title=\"Drag to reorder\"\n            >\n              <GripVertical className=\"h-4 w-4\" />\n            </button>\n          )}\n          <span className=\"text-sm font-medium truncate flex-1\" title={title}>\n            {title}\n          </span>\n        </div>\n        <div className=\"flex items-center gap-1\">\n          {/* Hover actions */}\n          {hasContent && (\n            <div className=\"flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity\">\n              <Button\n                onClick={() => setShowCode(!showCode)}\n                variant=\"ghost\"\n                size=\"sm\"\n                className={`h-6 w-6 p-0 text-muted-foreground hover:text-foreground ${showCode ? \"bg-muted\" : \"\"}`}\n                title={showCode ? \"Show preview\" : \"Show code\"}\n              >\n                <Code className=\"h-3.5 w-3.5\" />\n              </Button>\n              <Button\n                onClick={handleEdit}\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"h-6 w-6 p-0 text-muted-foreground hover:text-foreground\"\n                title=\"Edit\"\n              >\n                <Pencil className=\"h-3.5 w-3.5\" />\n              </Button>\n              <Button\n                onClick={() => setShowDeleteConfirm(true)}\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"h-6 w-6 p-0 text-muted-foreground hover:text-destructive\"\n                title=\"Delete\"\n              >\n                <Trash2 className=\"h-3.5 w-3.5\" />\n              </Button>\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Content area */}\n      <CardContent className=\"relative flex-1 overflow-auto p-4\">\n        {error ? (\n          <div className=\"text-destructive text-sm\">{error.message}</div>\n        ) : !hasContent && !isStreaming ? (\n          <div className=\"h-full flex flex-col items-center justify-center gap-3\">\n            <p className=\"text-muted-foreground text-sm\">Try one of these:</p>\n            <div className=\"flex flex-wrap gap-1.5 justify-center max-w-[300px]\">\n              {suggestions.map((s) => (\n                <Button\n                  key={s}\n                  onClick={() => handleGenerate(s)}\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"text-xs h-7\"\n                >\n                  {s}\n                </Button>\n              ))}\n            </div>\n          </div>\n        ) : currentSpec ? (\n          showCode ? (\n            <div className=\"relative h-full\">\n              <Button\n                onClick={handleCopyCode}\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"absolute top-0 right-0 h-7 w-7 p-0 text-muted-foreground hover:text-foreground\"\n                title=\"Copy code\"\n              >\n                {copied ? (\n                  <Check className=\"h-3.5 w-3.5 text-green-500\" />\n                ) : (\n                  <Copy className=\"h-3.5 w-3.5\" />\n                )}\n              </Button>\n              <CodeHighlight code={JSON.stringify(currentSpec, null, 2)} />\n            </div>\n          ) : (\n            <DashboardRenderer\n              spec={currentSpec}\n              state={state}\n              setState={setState}\n              onStateChange={handleStateChange}\n              loading={isStreaming}\n            />\n          )\n        ) : null}\n\n        {/* Edit overlay */}\n        {isEditing && (\n          <div className=\"absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-10\">\n            <div className=\"w-full max-w-sm px-4\">\n              <div className=\"flex items-center gap-2\">\n                <Input\n                  ref={editInputRef}\n                  type=\"text\"\n                  value={editPrompt}\n                  onChange={(e) => setEditPrompt(e.target.value)}\n                  onKeyDown={handleEditKeyDown}\n                  placeholder=\"Update widget prompt...\"\n                  className=\"flex-1 text-sm\"\n                  autoFocus\n                />\n                <Button onClick={handleEditSubmit} size=\"sm\">\n                  Go\n                </Button>\n                <Button\n                  onClick={() => setIsEditing(false)}\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"h-8 w-8 p-0\"\n                >\n                  <X className=\"h-4 w-4\" />\n                </Button>\n              </div>\n            </div>\n          </div>\n        )}\n      </CardContent>\n\n      {/* Prompt input at bottom - only show for new widgets */}\n      {!hasContent && (\n        <div className=\"p-3 border-t flex gap-2\">\n          <Input\n            ref={promptInputRef}\n            type=\"text\"\n            value={prompt}\n            onChange={(e) => setPrompt(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder=\"Describe this widget...\"\n            disabled={isStreaming}\n            className=\"flex-1 text-sm\"\n          />\n          <Button\n            onClick={() => handleGenerate()}\n            disabled={isStreaming || !prompt.trim()}\n            size=\"sm\"\n          >\n            {isStreaming ? \"...\" : \"Go\"}\n          </Button>\n        </div>\n      )}\n\n      {/* Delete confirmation dialog */}\n      <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Delete widget</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete this widget? This action cannot be\n              undone.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              onClick={() => setShowDeleteConfirm(false)}\n              variant=\"outline\"\n            >\n              Cancel\n            </Button>\n            <Button onClick={handleDelete} variant=\"destructive\">\n              Delete\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rtl\": false,\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "examples/dashboard/drizzle.config.ts",
    "content": "import { defineConfig } from \"drizzle-kit\";\n\nexport default defineConfig({\n  schema: \"./lib/db/schema.ts\",\n  out: \"./drizzle\",\n  dialect: \"postgresql\",\n  dbCredentials: {\n    url: process.env.DATABASE_URL!,\n  },\n});\n"
  },
  {
    "path": "examples/dashboard/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      // Disable prop-types - we use TypeScript for type checking\n      \"react/prop-types\": \"off\",\n      // Allow styled-jsx\n      \"react/no-unknown-property\": [\"error\", { ignore: [\"jsx\"] }],\n      // Allow DATABASE_URL env var\n      \"turbo/no-undeclared-env-vars\": [\n        \"error\",\n        { allowList: [\"DATABASE_URL\"] },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "examples/dashboard/lib/db/connection.ts",
    "content": "import { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport * as schema from \"./schema\";\n\n// Lazy initialization to allow builds without DATABASE_URL\nlet _db: ReturnType<typeof drizzle<typeof schema>> | null = null;\nlet _migrationClient: ReturnType<typeof postgres> | null = null;\n\nfunction getConnectionString(): string {\n  const connectionString = process.env.DATABASE_URL;\n  if (!connectionString) {\n    throw new Error(\"DATABASE_URL environment variable is not set\");\n  }\n  return connectionString;\n}\n\n// For query purposes - lazily initialized\nexport const db = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, {\n  get(_target, prop) {\n    if (!_db) {\n      const connectionString = getConnectionString();\n      const queryClient = postgres(connectionString);\n      _db = drizzle(queryClient, { schema });\n    }\n    return Reflect.get(_db, prop);\n  },\n});\n\n// For migrations - lazily initialized\nexport function getMigrationClient() {\n  if (!_migrationClient) {\n    const connectionString = getConnectionString();\n    _migrationClient = postgres(connectionString, { max: 1 });\n  }\n  return _migrationClient;\n}\n"
  },
  {
    "path": "examples/dashboard/lib/db/schema.ts",
    "content": "import {\n  pgTable,\n  varchar,\n  text,\n  integer,\n  decimal,\n  timestamp,\n  pgEnum,\n  jsonb,\n} from \"drizzle-orm/pg-core\";\n\n// Enums\nexport const customerStatusEnum = pgEnum(\"customer_status\", [\n  \"active\",\n  \"inactive\",\n]);\n\nexport const invoiceStatusEnum = pgEnum(\"invoice_status\", [\n  \"draft\",\n  \"sent\",\n  \"paid\",\n  \"overdue\",\n]);\n\nexport const expenseStatusEnum = pgEnum(\"expense_status\", [\n  \"pending\",\n  \"approved\",\n  \"rejected\",\n]);\n\nexport const accountTypeEnum = pgEnum(\"account_type\", [\n  \"bank\",\n  \"credit_card\",\n  \"cash\",\n]);\n\nexport const transactionTypeEnum = pgEnum(\"transaction_type\", [\n  \"income\",\n  \"expense\",\n  \"transfer\",\n]);\n\n// Tables\nexport const customers = pgTable(\"customers\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  name: varchar(\"name\", { length: 255 }).notNull(),\n  email: varchar(\"email\", { length: 255 }).notNull(),\n  phone: varchar(\"phone\", { length: 50 }),\n  balance: decimal(\"balance\", { precision: 12, scale: 2 })\n    .notNull()\n    .default(\"0\"),\n  status: customerStatusEnum(\"status\").notNull().default(\"active\"),\n  createdAt: timestamp(\"created_at\").notNull().defaultNow(),\n});\n\nexport const invoices = pgTable(\"invoices\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  customerId: varchar(\"customer_id\", { length: 50 })\n    .notNull()\n    .references(() => customers.id),\n  customerName: varchar(\"customer_name\", { length: 255 }).notNull(),\n  amount: decimal(\"amount\", { precision: 12, scale: 2 }).notNull(),\n  status: invoiceStatusEnum(\"status\").notNull().default(\"draft\"),\n  dueDate: timestamp(\"due_date\").notNull(),\n  createdAt: timestamp(\"created_at\").notNull().defaultNow(),\n  items: jsonb(\"items\").$type<InvoiceItem[]>().notNull().default([]),\n});\n\nexport const expenses = pgTable(\"expenses\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  vendor: varchar(\"vendor\", { length: 255 }).notNull(),\n  category: varchar(\"category\", { length: 100 }).notNull(),\n  amount: decimal(\"amount\", { precision: 12, scale: 2 }).notNull(),\n  date: timestamp(\"date\").notNull(),\n  status: expenseStatusEnum(\"status\").notNull().default(\"pending\"),\n  description: text(\"description\"),\n});\n\nexport const accounts = pgTable(\"accounts\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  name: varchar(\"name\", { length: 255 }).notNull(),\n  type: accountTypeEnum(\"type\").notNull(),\n  balance: decimal(\"balance\", { precision: 12, scale: 2 }).notNull(),\n  lastSync: timestamp(\"last_sync\").notNull().defaultNow(),\n});\n\nexport const transactions = pgTable(\"transactions\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  accountId: varchar(\"account_id\", { length: 50 })\n    .notNull()\n    .references(() => accounts.id),\n  type: transactionTypeEnum(\"type\").notNull(),\n  amount: decimal(\"amount\", { precision: 12, scale: 2 }).notNull(),\n  description: text(\"description\").notNull(),\n  category: varchar(\"category\", { length: 100 }).notNull(),\n  date: timestamp(\"date\").notNull(),\n});\n\nexport const widgets = pgTable(\"widgets\", {\n  id: varchar(\"id\", { length: 50 }).primaryKey(),\n  prompt: text(\"prompt\").notNull(),\n  spec: jsonb(\"spec\").$type<WidgetSpec>().notNull(),\n  order: integer(\"order\").notNull().default(0),\n  createdAt: timestamp(\"created_at\").notNull().defaultNow(),\n  updatedAt: timestamp(\"updated_at\").notNull().defaultNow(),\n});\n\n// Types\nexport interface WidgetSpec {\n  root: string;\n  elements: Record<string, unknown>;\n}\nexport interface InvoiceItem {\n  description: string;\n  quantity: number;\n  rate: number;\n  amount: number;\n}\n\nexport type Customer = typeof customers.$inferSelect;\nexport type NewCustomer = typeof customers.$inferInsert;\n\nexport type Invoice = typeof invoices.$inferSelect;\nexport type NewInvoice = typeof invoices.$inferInsert;\n\nexport type Expense = typeof expenses.$inferSelect;\nexport type NewExpense = typeof expenses.$inferInsert;\n\nexport type Account = typeof accounts.$inferSelect;\nexport type Transaction = typeof transactions.$inferSelect;\n\nexport type Widget = typeof widgets.$inferSelect;\nexport type NewWidget = typeof widgets.$inferInsert;\n"
  },
  {
    "path": "examples/dashboard/lib/db/store.ts",
    "content": "/**\n * Database store for the dashboard API using Drizzle ORM\n */\n\nimport { eq, ilike, or, desc, asc, max } from \"drizzle-orm\";\nimport { db } from \"./connection\";\nimport {\n  customers,\n  invoices,\n  expenses,\n  accounts,\n  transactions,\n  widgets,\n  type Customer,\n  type Invoice,\n  type Expense,\n  type InvoiceItem,\n  type WidgetSpec,\n} from \"./schema\";\n\n// Helper to generate IDs\nconst generateId = (prefix: string) => `${prefix}-${Date.now()}`;\n\n// Customer methods\nexport async function getCustomers(filters?: {\n  status?: string;\n  search?: string;\n  limit?: number;\n  sort?: \"newest\" | \"oldest\";\n}) {\n  let query = db.select().from(customers);\n\n  if (filters?.status) {\n    query = query.where(\n      eq(customers.status, filters.status as \"active\" | \"inactive\"),\n    ) as typeof query;\n  }\n\n  if (filters?.search) {\n    const searchPattern = `%${filters.search}%`;\n    query = query.where(\n      or(\n        ilike(customers.name, searchPattern),\n        ilike(customers.email, searchPattern),\n      ),\n    ) as typeof query;\n  }\n\n  // Default to newest first\n  const sortOrder = filters?.sort === \"oldest\" ? asc : desc;\n  query = query.orderBy(sortOrder(customers.createdAt)) as typeof query;\n\n  if (filters?.limit) {\n    query = query.limit(filters.limit) as typeof query;\n  }\n\n  return query;\n}\n\nexport async function getCustomer(id: string) {\n  const result = await db\n    .select()\n    .from(customers)\n    .where(eq(customers.id, id))\n    .limit(1);\n  return result[0] || null;\n}\n\nexport async function createCustomer(data: {\n  name: string;\n  email: string;\n  phone?: string;\n}) {\n  const id = generateId(\"cust\");\n  const [customer] = await db\n    .insert(customers)\n    .values({\n      id,\n      name: data.name,\n      email: data.email,\n      phone: data.phone || null,\n      balance: \"0\",\n      status: \"active\",\n    })\n    .returning();\n  return customer;\n}\n\nexport async function updateCustomer(id: string, data: Partial<Customer>) {\n  const [customer] = await db\n    .update(customers)\n    .set(data)\n    .where(eq(customers.id, id))\n    .returning();\n  return customer || null;\n}\n\nexport async function deleteCustomer(id: string) {\n  const result = await db\n    .delete(customers)\n    .where(eq(customers.id, id))\n    .returning({ id: customers.id });\n  return { success: result.length > 0 };\n}\n\n// Invoice methods\nexport async function getInvoices(filters?: {\n  status?: string;\n  customerId?: string;\n}) {\n  let query = db.select().from(invoices);\n\n  if (filters?.status) {\n    query = query.where(\n      eq(invoices.status, filters.status as Invoice[\"status\"]),\n    ) as typeof query;\n  }\n\n  if (filters?.customerId) {\n    query = query.where(\n      eq(invoices.customerId, filters.customerId),\n    ) as typeof query;\n  }\n\n  return query;\n}\n\nexport async function getInvoice(id: string) {\n  const result = await db\n    .select()\n    .from(invoices)\n    .where(eq(invoices.id, id))\n    .limit(1);\n  return result[0] || null;\n}\n\nexport async function createInvoice(data: {\n  customerId: string;\n  dueDate: string;\n  items?: InvoiceItem[];\n}) {\n  const customer = await getCustomer(data.customerId);\n  if (!customer) return null;\n\n  const items = data.items || [];\n  const amount = items.reduce((sum, item) => sum + item.amount, 0);\n\n  const id = generateId(\"inv\");\n  const [invoice] = await db\n    .insert(invoices)\n    .values({\n      id,\n      customerId: data.customerId,\n      customerName: customer.name,\n      amount: amount.toString(),\n      status: \"draft\",\n      dueDate: new Date(data.dueDate),\n      items,\n    })\n    .returning();\n  return invoice;\n}\n\nexport async function updateInvoice(id: string, data: Partial<Invoice>) {\n  const [invoice] = await db\n    .update(invoices)\n    .set(data)\n    .where(eq(invoices.id, id))\n    .returning();\n  return invoice || null;\n}\n\nexport async function deleteInvoice(id: string) {\n  const result = await db\n    .delete(invoices)\n    .where(eq(invoices.id, id))\n    .returning({ id: invoices.id });\n  return result.length > 0;\n}\n\nexport async function sendInvoice(id: string) {\n  const invoice = await getInvoice(id);\n  if (!invoice) return { error: \"Invoice not found\" };\n  if (invoice.status !== \"draft\")\n    return { error: \"Only draft invoices can be sent\" };\n  return updateInvoice(id, { status: \"sent\" });\n}\n\nexport async function markInvoicePaid(id: string) {\n  const invoice = await getInvoice(id);\n  if (!invoice) return { error: \"Invoice not found\" };\n  if (invoice.status === \"paid\") return { error: \"Invoice is already paid\" };\n\n  // Update customer balance\n  const customer = await getCustomer(invoice.customerId);\n  if (customer) {\n    const newBalance =\n      parseFloat(customer.balance as string) -\n      parseFloat(invoice.amount as string);\n    await updateCustomer(invoice.customerId, {\n      balance: newBalance.toString(),\n    });\n  }\n\n  // Add transaction\n  await db.insert(transactions).values({\n    id: generateId(\"txn\"),\n    accountId: \"acc-001\",\n    type: \"income\",\n    amount: invoice.amount,\n    description: `Invoice #${invoice.id} payment from ${invoice.customerName}`,\n    category: \"Sales\",\n    date: new Date(),\n  });\n\n  return updateInvoice(id, { status: \"paid\" });\n}\n\n// Expense methods\nexport async function getExpenses(filters?: {\n  status?: string;\n  category?: string;\n}) {\n  let query = db.select().from(expenses);\n\n  if (filters?.status) {\n    query = query.where(\n      eq(expenses.status, filters.status as Expense[\"status\"]),\n    ) as typeof query;\n  }\n\n  if (filters?.category) {\n    query = query.where(\n      eq(expenses.category, filters.category),\n    ) as typeof query;\n  }\n\n  return query;\n}\n\nexport async function getExpense(id: string) {\n  const result = await db\n    .select()\n    .from(expenses)\n    .where(eq(expenses.id, id))\n    .limit(1);\n  return result[0] || null;\n}\n\nexport async function createExpense(data: {\n  vendor: string;\n  category: string;\n  amount: number;\n  date?: string;\n  description?: string;\n}) {\n  const id = generateId(\"exp\");\n  const [expense] = await db\n    .insert(expenses)\n    .values({\n      id,\n      vendor: data.vendor,\n      category: data.category,\n      amount: data.amount.toString(),\n      date: data.date ? new Date(data.date) : new Date(),\n      status: \"pending\",\n      description: data.description || null,\n    })\n    .returning();\n  return expense;\n}\n\nexport async function updateExpense(id: string, data: Partial<Expense>) {\n  const [expense] = await db\n    .update(expenses)\n    .set(data)\n    .where(eq(expenses.id, id))\n    .returning();\n  return expense || null;\n}\n\nexport async function deleteExpense(id: string) {\n  const result = await db\n    .delete(expenses)\n    .where(eq(expenses.id, id))\n    .returning({ id: expenses.id });\n  return result.length > 0;\n}\n\nexport async function approveExpense(id: string) {\n  const expense = await getExpense(id);\n  if (!expense) return { error: \"Expense not found\" };\n  if (expense.status !== \"pending\")\n    return { error: \"Only pending expenses can be approved\" };\n\n  // Add transaction\n  await db.insert(transactions).values({\n    id: generateId(\"txn\"),\n    accountId: \"acc-001\",\n    type: \"expense\",\n    amount: expense.amount,\n    description: `${expense.vendor} - ${expense.description || \"\"}`,\n    category: expense.category,\n    date: expense.date,\n  });\n\n  return updateExpense(id, { status: \"approved\" });\n}\n\nexport async function rejectExpense(id: string) {\n  const expense = await getExpense(id);\n  if (!expense) return { error: \"Expense not found\" };\n  if (expense.status !== \"pending\")\n    return { error: \"Only pending expenses can be rejected\" };\n  return updateExpense(id, { status: \"rejected\" });\n}\n\n// Account methods\nexport async function getAccounts() {\n  const accountList = await db.select().from(accounts);\n\n  // Get recent transactions for each account\n  const result = await Promise.all(\n    accountList.map(async (account) => {\n      const recentTxns = await db\n        .select()\n        .from(transactions)\n        .where(eq(transactions.accountId, account.id))\n        .orderBy(desc(transactions.date))\n        .limit(5);\n      return { ...account, recentTransactions: recentTxns };\n    }),\n  );\n\n  return result;\n}\n\nexport async function getAccount(id: string) {\n  const result = await db\n    .select()\n    .from(accounts)\n    .where(eq(accounts.id, id))\n    .limit(1);\n\n  if (!result[0]) return null;\n\n  const txns = await db\n    .select()\n    .from(transactions)\n    .where(eq(transactions.accountId, id))\n    .orderBy(desc(transactions.date));\n\n  return { ...result[0], transactions: txns };\n}\n\n// Dashboard summary\nexport async function getDashboardSummary() {\n  // Get all invoices\n  const allInvoices = await db.select().from(invoices);\n  const paidInvoices = allInvoices.filter((i) => i.status === \"paid\");\n  const totalRevenue = paidInvoices.reduce(\n    (sum, i) => sum + parseFloat(i.amount as string),\n    0,\n  );\n\n  const outstandingInvoices = allInvoices\n    .filter((i) => i.status === \"sent\" || i.status === \"overdue\")\n    .reduce((sum, i) => sum + parseFloat(i.amount as string), 0);\n\n  const overdueAmount = allInvoices\n    .filter((i) => i.status === \"overdue\")\n    .reduce((sum, i) => sum + parseFloat(i.amount as string), 0);\n\n  // Get all expenses\n  const allExpenses = await db.select().from(expenses);\n  const approvedExpenses = allExpenses.filter((e) => e.status === \"approved\");\n  const totalExpenses = approvedExpenses.reduce(\n    (sum, e) => sum + parseFloat(e.amount as string),\n    0,\n  );\n\n  const cashFlow = totalRevenue - totalExpenses;\n\n  // Get accounts\n  const allAccounts = await db.select().from(accounts);\n  const totalBankBalance = allAccounts\n    .filter((a) => a.type === \"bank\")\n    .reduce((sum, a) => sum + parseFloat(a.balance as string), 0);\n\n  // Get customers\n  const allCustomers = await db.select().from(customers);\n\n  // Get recent transactions\n  const recentTxns = await db\n    .select()\n    .from(transactions)\n    .orderBy(desc(transactions.date))\n    .limit(5);\n\n  // Group expenses by category\n  const expensesByCategory = approvedExpenses.reduce(\n    (acc, e) => {\n      acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);\n      return acc;\n    },\n    {} as Record<string, number>,\n  );\n\n  return {\n    revenue: {\n      total: totalRevenue,\n      outstanding: outstandingInvoices,\n      overdue: overdueAmount,\n    },\n    expenses: {\n      total: totalExpenses,\n      pending: allExpenses.filter((e) => e.status === \"pending\").length,\n    },\n    cashFlow,\n    bankBalance: totalBankBalance,\n    customers: {\n      total: allCustomers.length,\n      active: allCustomers.filter((c) => c.status === \"active\").length,\n    },\n    invoices: {\n      total: allInvoices.length,\n      draft: allInvoices.filter((i) => i.status === \"draft\").length,\n      sent: allInvoices.filter((i) => i.status === \"sent\").length,\n      paid: allInvoices.filter((i) => i.status === \"paid\").length,\n      overdue: allInvoices.filter((i) => i.status === \"overdue\").length,\n    },\n    recentTransactions: recentTxns.map((t) => ({\n      ...t,\n      amount: parseFloat(t.amount as string),\n    })),\n    expensesByCategory: Object.entries(expensesByCategory).map(\n      ([label, value]) => ({\n        label,\n        value,\n      }),\n    ),\n    revenueByMonth: [\n      { label: \"Oct\", value: 18000 },\n      { label: \"Nov\", value: 22000 },\n      { label: \"Dec\", value: 28000 },\n      { label: \"Jan\", value: 35000 },\n      { label: \"Feb\", value: totalRevenue },\n    ],\n  };\n}\n\n// Reports\nexport async function getProfitLossReport(\n  startDate?: string,\n  endDate?: string,\n) {\n  const start = startDate ? new Date(startDate) : new Date(\"2024-01-01\");\n  const end = endDate ? new Date(endDate) : new Date(\"2024-12-31\");\n\n  const allInvoices = await db.select().from(invoices);\n  const paidInvoices = allInvoices.filter(\n    (i) => i.status === \"paid\" && i.createdAt >= start && i.createdAt <= end,\n  );\n\n  const allExpenses = await db.select().from(expenses);\n  const approvedExpenses = allExpenses.filter(\n    (e) => e.status === \"approved\" && e.date >= start && e.date <= end,\n  );\n\n  const totalIncome = paidInvoices.reduce(\n    (sum, i) => sum + parseFloat(i.amount as string),\n    0,\n  );\n  const totalExpensesAmount = approvedExpenses.reduce(\n    (sum, e) => sum + parseFloat(e.amount as string),\n    0,\n  );\n  const netProfit = totalIncome - totalExpensesAmount;\n\n  const expensesByCategory = approvedExpenses.reduce(\n    (acc, e) => {\n      acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);\n      return acc;\n    },\n    {} as Record<string, number>,\n  );\n\n  return {\n    period: {\n      startDate: start.toISOString().split(\"T\")[0],\n      endDate: end.toISOString().split(\"T\")[0],\n    },\n    income: {\n      total: totalIncome,\n      invoiceCount: paidInvoices.length,\n    },\n    expenses: {\n      total: totalExpensesAmount,\n      expenseCount: approvedExpenses.length,\n      byCategory: Object.entries(expensesByCategory).map(\n        ([category, amount]) => ({\n          category,\n          amount,\n        }),\n      ),\n    },\n    netProfit,\n    profitMargin: totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0,\n  };\n}\n\n// Clear database (delete all data)\nexport async function clearDatabase() {\n  await db.delete(transactions);\n  await db.delete(invoices);\n  await db.delete(expenses);\n  await db.delete(customers);\n  await db.delete(accounts);\n  return { message: \"Database cleared successfully\" };\n}\n\n// Reset/seed database\nexport async function resetDatabase() {\n  // Delete all data\n  await clearDatabase();\n\n  // Seed accounts first\n  await db.insert(accounts).values([\n    {\n      id: \"acc-001\",\n      name: \"Business Checking\",\n      type: \"bank\",\n      balance: \"125000\",\n      lastSync: new Date(),\n    },\n    {\n      id: \"acc-002\",\n      name: \"Business Savings\",\n      type: \"bank\",\n      balance: \"75000\",\n      lastSync: new Date(),\n    },\n    {\n      id: \"acc-003\",\n      name: \"Corporate Card\",\n      type: \"credit_card\",\n      balance: \"-8500\",\n      lastSync: new Date(),\n    },\n    {\n      id: \"acc-004\",\n      name: \"Petty Cash\",\n      type: \"cash\",\n      balance: \"500\",\n      lastSync: new Date(),\n    },\n  ]);\n\n  // Seed customers with dates spread over past 30 days\n  const now = new Date();\n  const daysAgo = (days: number) =>\n    new Date(now.getTime() - days * 24 * 60 * 60 * 1000);\n\n  const seedCustomers = [\n    // Enterprise customers (older)\n    {\n      id: \"cust-001\",\n      name: \"Acme Corporation\",\n      email: \"billing@acme.com\",\n      phone: \"(555) 123-4567\",\n      balance: \"15000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(28),\n    },\n    {\n      id: \"cust-002\",\n      name: \"Globex Industries\",\n      email: \"ap@globex.com\",\n      phone: \"(555) 234-5678\",\n      balance: \"8500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(27),\n    },\n    {\n      id: \"cust-003\",\n      name: \"Initech LLC\",\n      email: \"finance@initech.com\",\n      phone: \"(555) 345-6789\",\n      balance: \"2300\",\n      status: \"active\" as const,\n      createdAt: daysAgo(25),\n    },\n    {\n      id: \"cust-004\",\n      name: \"Umbrella Corp\",\n      email: \"accounts@umbrella.com\",\n      phone: \"(555) 456-7890\",\n      balance: \"45000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(24),\n    },\n    {\n      id: \"cust-005\",\n      name: \"Stark Industries\",\n      email: \"billing@stark.com\",\n      phone: \"(555) 567-8901\",\n      balance: \"0\",\n      status: \"inactive\" as const,\n      createdAt: daysAgo(23),\n    },\n    // Week 3 signups\n    {\n      id: \"cust-006\",\n      name: \"Wayne Enterprises\",\n      email: \"finance@wayne.com\",\n      phone: \"(555) 678-9012\",\n      balance: \"32000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(21),\n    },\n    {\n      id: \"cust-007\",\n      name: \"Oscorp\",\n      email: \"billing@oscorp.com\",\n      phone: \"(555) 789-0123\",\n      balance: \"12500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(20),\n    },\n    {\n      id: \"cust-008\",\n      name: \"LexCorp\",\n      email: \"accounts@lexcorp.com\",\n      phone: \"(555) 890-1234\",\n      balance: \"67000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(19),\n    },\n    {\n      id: \"cust-009\",\n      name: \"Cyberdyne Systems\",\n      email: \"ap@cyberdyne.com\",\n      phone: \"(555) 901-2345\",\n      balance: \"4500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(18),\n    },\n    {\n      id: \"cust-010\",\n      name: \"Weyland-Yutani\",\n      email: \"billing@weyland.com\",\n      phone: \"(555) 012-3456\",\n      balance: \"89000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(17),\n    },\n    // Week 2 signups (more activity)\n    {\n      id: \"cust-011\",\n      name: \"Tyrell Corp\",\n      email: \"finance@tyrell.com\",\n      phone: \"(555) 111-2222\",\n      balance: \"23000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(14),\n    },\n    {\n      id: \"cust-012\",\n      name: \"Soylent Corp\",\n      email: \"billing@soylent.com\",\n      phone: \"(555) 222-3333\",\n      balance: \"5600\",\n      status: \"active\" as const,\n      createdAt: daysAgo(14),\n    },\n    {\n      id: \"cust-013\",\n      name: \"Massive Dynamic\",\n      email: \"accounts@massive.com\",\n      phone: \"(555) 333-4444\",\n      balance: \"41000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(13),\n    },\n    {\n      id: \"cust-014\",\n      name: \"Aperture Science\",\n      email: \"billing@aperture.com\",\n      phone: \"(555) 444-5555\",\n      balance: \"0\",\n      status: \"inactive\" as const,\n      createdAt: daysAgo(12),\n    },\n    {\n      id: \"cust-015\",\n      name: \"Black Mesa\",\n      email: \"finance@blackmesa.com\",\n      phone: \"(555) 555-6666\",\n      balance: \"18500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(11),\n    },\n    {\n      id: \"cust-016\",\n      name: \"Vault-Tec\",\n      email: \"sales@vaulttec.com\",\n      phone: \"(555) 666-7777\",\n      balance: \"72000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(10),\n    },\n    {\n      id: \"cust-017\",\n      name: \"Abstergo Industries\",\n      email: \"billing@abstergo.com\",\n      phone: \"(555) 777-8888\",\n      balance: \"9800\",\n      status: \"active\" as const,\n      createdAt: daysAgo(10),\n    },\n    {\n      id: \"cust-018\",\n      name: \"Shinra Electric\",\n      email: \"accounts@shinra.com\",\n      phone: \"(555) 888-9999\",\n      balance: \"125000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(9),\n    },\n    // Week 1 signups (high activity)\n    {\n      id: \"cust-019\",\n      name: \"Monsters Inc\",\n      email: \"billing@monsters.com\",\n      phone: \"(555) 999-0000\",\n      balance: \"34500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(7),\n    },\n    {\n      id: \"cust-020\",\n      name: \"Buy n Large\",\n      email: \"finance@bnl.com\",\n      phone: \"(555) 000-1111\",\n      balance: \"56000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(7),\n    },\n    {\n      id: \"cust-021\",\n      name: \"Omni Consumer\",\n      email: \"ap@omni.com\",\n      phone: \"(555) 121-2121\",\n      balance: \"8900\",\n      status: \"active\" as const,\n      createdAt: daysAgo(6),\n    },\n    {\n      id: \"cust-022\",\n      name: \"Rekall Inc\",\n      email: \"billing@rekall.com\",\n      phone: \"(555) 131-3131\",\n      balance: \"15600\",\n      status: \"active\" as const,\n      createdAt: daysAgo(5),\n    },\n    {\n      id: \"cust-023\",\n      name: \"Virtucon\",\n      email: \"accounts@virtucon.com\",\n      phone: \"(555) 141-4141\",\n      balance: \"28000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(5),\n    },\n    {\n      id: \"cust-024\",\n      name: \"Wonka Industries\",\n      email: \"finance@wonka.com\",\n      phone: \"(555) 151-5151\",\n      balance: \"47000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(4),\n    },\n    {\n      id: \"cust-025\",\n      name: \"Dunder Mifflin\",\n      email: \"sales@dundermifflin.com\",\n      phone: \"(555) 161-6161\",\n      balance: \"3200\",\n      status: \"active\" as const,\n      createdAt: daysAgo(4),\n    },\n    {\n      id: \"cust-026\",\n      name: \"Sterling Cooper\",\n      email: \"billing@sterlingcooper.com\",\n      phone: \"(555) 171-7171\",\n      balance: \"19500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(3),\n    },\n    // Recent signups (last few days)\n    {\n      id: \"cust-027\",\n      name: \"Prestige Worldwide\",\n      email: \"boats@prestige.com\",\n      phone: \"(555) 181-8181\",\n      balance: \"0\",\n      status: \"active\" as const,\n      createdAt: daysAgo(2),\n    },\n    {\n      id: \"cust-028\",\n      name: \"Pied Piper\",\n      email: \"richard@piedpiper.com\",\n      phone: \"(555) 191-9191\",\n      balance: \"500\",\n      status: \"active\" as const,\n      createdAt: daysAgo(2),\n    },\n    {\n      id: \"cust-029\",\n      name: \"Hooli\",\n      email: \"billing@hooli.com\",\n      phone: \"(555) 202-0202\",\n      balance: \"250000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(1),\n    },\n    {\n      id: \"cust-030\",\n      name: \"Aviato\",\n      email: \"erlich@aviato.com\",\n      phone: \"(555) 212-1212\",\n      balance: \"1200\",\n      status: \"active\" as const,\n      createdAt: daysAgo(1),\n    },\n    {\n      id: \"cust-031\",\n      name: \"Bluth Company\",\n      email: \"gob@bluth.com\",\n      phone: \"(555) 222-2222\",\n      balance: \"0\",\n      status: \"inactive\" as const,\n      createdAt: daysAgo(0),\n    },\n    {\n      id: \"cust-032\",\n      name: \"Los Pollos Hermanos\",\n      email: \"gus@lospollos.com\",\n      phone: \"(555) 232-3232\",\n      balance: \"98000\",\n      status: \"active\" as const,\n      createdAt: daysAgo(0),\n    },\n  ];\n\n  await db.insert(customers).values(seedCustomers);\n\n  // Seed invoices\n  await db.insert(invoices).values([\n    {\n      id: \"inv-001\",\n      customerId: \"cust-001\",\n      customerName: \"Acme Corporation\",\n      amount: \"5000\",\n      status: \"paid\",\n      dueDate: new Date(\"2024-02-15\"),\n      items: [\n        {\n          description: \"Consulting Services\",\n          quantity: 20,\n          rate: 250,\n          amount: 5000,\n        },\n      ],\n    },\n    {\n      id: \"inv-002\",\n      customerId: \"cust-002\",\n      customerName: \"Globex Industries\",\n      amount: \"3500\",\n      status: \"sent\",\n      dueDate: new Date(\"2024-02-28\"),\n      items: [\n        {\n          description: \"Software License\",\n          quantity: 7,\n          rate: 500,\n          amount: 3500,\n        },\n      ],\n    },\n    {\n      id: \"inv-003\",\n      customerId: \"cust-001\",\n      customerName: \"Acme Corporation\",\n      amount: \"10000\",\n      status: \"overdue\",\n      dueDate: new Date(\"2024-01-31\"),\n      items: [\n        {\n          description: \"Annual Support Contract\",\n          quantity: 1,\n          rate: 10000,\n          amount: 10000,\n        },\n      ],\n    },\n    {\n      id: \"inv-004\",\n      customerId: \"cust-003\",\n      customerName: \"Initech LLC\",\n      amount: \"2300\",\n      status: \"draft\",\n      dueDate: new Date(\"2024-03-15\"),\n      items: [\n        {\n          description: \"Training Session\",\n          quantity: 1,\n          rate: 2300,\n          amount: 2300,\n        },\n      ],\n    },\n    {\n      id: \"inv-005\",\n      customerId: \"cust-004\",\n      customerName: \"Umbrella Corp\",\n      amount: \"45000\",\n      status: \"sent\",\n      dueDate: new Date(\"2024-03-01\"),\n      items: [\n        {\n          description: \"Enterprise License\",\n          quantity: 1,\n          rate: 45000,\n          amount: 45000,\n        },\n      ],\n    },\n  ]);\n\n  // Seed expenses\n  await db.insert(expenses).values([\n    {\n      id: \"exp-001\",\n      vendor: \"AWS\",\n      category: \"Software\",\n      amount: \"2500\",\n      date: new Date(\"2024-02-01\"),\n      status: \"approved\",\n      description: \"Cloud hosting - February\",\n    },\n    {\n      id: \"exp-002\",\n      vendor: \"Office Depot\",\n      category: \"Office Supplies\",\n      amount: \"350\",\n      date: new Date(\"2024-02-05\"),\n      status: \"approved\",\n      description: \"Printer paper and toner\",\n    },\n    {\n      id: \"exp-003\",\n      vendor: \"Delta Airlines\",\n      category: \"Travel\",\n      amount: \"890\",\n      date: new Date(\"2024-02-10\"),\n      status: \"pending\",\n      description: \"Flight to client meeting\",\n    },\n    {\n      id: \"exp-004\",\n      vendor: \"WeWork\",\n      category: \"Rent\",\n      amount: \"4500\",\n      date: new Date(\"2024-02-01\"),\n      status: \"approved\",\n      description: \"Office space - February\",\n    },\n    {\n      id: \"exp-005\",\n      vendor: \"Uber\",\n      category: \"Travel\",\n      amount: \"125\",\n      date: new Date(\"2024-02-12\"),\n      status: \"pending\",\n      description: \"Client site visits\",\n    },\n  ]);\n\n  // Seed transactions\n  await db.insert(transactions).values([\n    {\n      id: \"txn-001\",\n      accountId: \"acc-001\",\n      type: \"income\",\n      amount: \"5000\",\n      description: \"Invoice #inv-001 payment\",\n      category: \"Sales\",\n      date: new Date(\"2024-02-10\"),\n    },\n    {\n      id: \"txn-002\",\n      accountId: \"acc-001\",\n      type: \"expense\",\n      amount: \"2500\",\n      description: \"AWS hosting\",\n      category: \"Software\",\n      date: new Date(\"2024-02-01\"),\n    },\n    {\n      id: \"txn-003\",\n      accountId: \"acc-003\",\n      type: \"expense\",\n      amount: \"350\",\n      description: \"Office supplies\",\n      category: \"Office\",\n      date: new Date(\"2024-02-05\"),\n    },\n    {\n      id: \"txn-004\",\n      accountId: \"acc-001\",\n      type: \"income\",\n      amount: \"15000\",\n      description: \"Consulting retainer\",\n      category: \"Sales\",\n      date: new Date(\"2024-02-01\"),\n    },\n    {\n      id: \"txn-005\",\n      accountId: \"acc-001\",\n      type: \"expense\",\n      amount: \"4500\",\n      description: \"Office rent\",\n      category: \"Rent\",\n      date: new Date(\"2024-02-01\"),\n    },\n  ]);\n\n  return { message: \"Database reset and seeded successfully\" };\n}\n\n// Widget methods\nexport async function getWidgets() {\n  return db.select().from(widgets).orderBy(asc(widgets.order));\n}\n\nexport async function getWidget(id: string) {\n  const result = await db\n    .select()\n    .from(widgets)\n    .where(eq(widgets.id, id))\n    .limit(1);\n  return result[0] || null;\n}\n\nexport async function createWidget(data: { prompt: string; spec: WidgetSpec }) {\n  const id = generateId(\"widget\");\n\n  // Get the next order value\n  const [result] = await db\n    .select({ maxOrder: max(widgets.order) })\n    .from(widgets);\n  const nextOrder = (result?.maxOrder ?? -1) + 1;\n\n  const [widget] = await db\n    .insert(widgets)\n    .values({\n      id,\n      prompt: data.prompt,\n      spec: data.spec,\n      order: nextOrder,\n    })\n    .returning();\n  return widget;\n}\n\nexport async function updateWidget(\n  id: string,\n  data: { prompt?: string; spec?: WidgetSpec },\n) {\n  const [widget] = await db\n    .update(widgets)\n    .set({ ...data, updatedAt: new Date() })\n    .where(eq(widgets.id, id))\n    .returning();\n  return widget || null;\n}\n\nexport async function deleteWidget(id: string) {\n  const result = await db\n    .delete(widgets)\n    .where(eq(widgets.id, id))\n    .returning({ id: widgets.id });\n  return result.length > 0;\n}\n\nexport async function reorderWidgets(orderedIds: string[]) {\n  // Update each widget's order based on position in array\n  for (let i = 0; i < orderedIds.length; i++) {\n    const id = orderedIds[i];\n    if (!id) continue;\n    await db.update(widgets).set({ order: i }).where(eq(widgets.id, id));\n  }\n}\n"
  },
  {
    "path": "examples/dashboard/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:dashboard:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:dashboard:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/dashboard/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\n/**\n * Dashboard Catalog\n *\n * Components map directly to shadcn/ui components.\n * Actions correspond to real API endpoints in /api/v1/*\n */\nexport const dashboardCatalog = defineCatalog(schema, {\n  components: {\n    // Layout\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n        gap: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Flex layout container\",\n      example: { direction: \"vertical\", gap: \"md\" },\n    },\n\n    Accordion: {\n      props: z.object({\n        type: z.enum([\"single\", \"multiple\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Collapsible accordion container\",\n    },\n\n    AccordionItem: {\n      props: z.object({\n        value: z.string(),\n        title: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Accordion item with trigger and content\",\n    },\n\n    // Form\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z\n          .enum([\"default\", \"secondary\", \"destructive\", \"outline\", \"ghost\"])\n          .nullable(),\n        action: z.string(),\n        actionParams: z.record(z.string(), z.unknown()).nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Clickable button. Use actionParams to pass parameters to the action (e.g., { limit: 5, sort: 'newest' })\",\n      example: { label: \"Save\", variant: \"default\", action: \"formSubmit\" },\n    },\n\n    Input: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        type: z.enum([\"text\", \"email\", \"password\", \"number\", \"tel\"]).nullable(),\n      }),\n      description:\n        \"Text input field. Use value with $bindState for two-way binding\",\n      example: {\n        label: \"Email\",\n        value: { $bindState: \"/form/email\" },\n        placeholder: \"you@example.com\",\n        type: \"email\",\n      },\n    },\n\n    Form: {\n      props: z.object({\n        submitAction: z.string(),\n        submitActionParams: z.record(z.string(), z.unknown()).nullable(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Form container that enables Enter key submission. Wrap form inputs (Input, Select, Checkbox, etc.) and a submit Button inside this component.\",\n    },\n\n    // Display\n    Badge: {\n      props: z.object({\n        text: z.string(),\n        variant: z\n          .enum([\"default\", \"secondary\", \"destructive\", \"outline\"])\n          .nullable(),\n      }),\n      description: \"Status badge\",\n      example: { text: \"Active\", variant: \"default\" },\n    },\n\n    Alert: {\n      props: z.object({\n        variant: z.enum([\"default\", \"destructive\"]).nullable(),\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Alert message\",\n    },\n\n    Separator: {\n      props: z.object({}),\n      description: \"Visual divider\",\n    },\n\n    Avatar: {\n      props: z.object({\n        src: z.string().nullable(),\n        alt: z.string().nullable(),\n        fallback: z.string(),\n      }),\n      description: \"User avatar image with fallback initials\",\n    },\n\n    Checkbox: {\n      props: z.object({\n        label: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n      }),\n      description:\n        \"Checkbox input. Use checked with $bindState for two-way binding\",\n    },\n\n    Dialog: {\n      props: z.object({\n        trigger: z.string(),\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Modal dialog with trigger button\",\n    },\n\n    Drawer: {\n      props: z.object({\n        trigger: z.string(),\n        title: z.string(),\n        description: z.string().nullable(),\n        side: z.enum([\"top\", \"bottom\", \"left\", \"right\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Slide-out drawer panel\",\n    },\n\n    DropdownMenu: {\n      props: z.object({\n        trigger: z.string(),\n        items: z.array(\n          z.object({\n            label: z.string(),\n            action: z.string().nullable(),\n            actionParams: z.record(z.string(), z.unknown()).nullable(),\n          }),\n        ),\n      }),\n      description: \"Dropdown menu with action items\",\n    },\n\n    Label: {\n      props: z.object({\n        text: z.string(),\n        htmlFor: z.string().nullable(),\n      }),\n      description: \"Form label\",\n    },\n\n    Pagination: {\n      props: z.object({\n        currentPage: z.number(),\n        totalPages: z.number(),\n        onPageChange: z.string().nullable(),\n      }),\n      description: \"Page navigation\",\n    },\n\n    Popover: {\n      props: z.object({\n        trigger: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Popover with trigger\",\n    },\n\n    Progress: {\n      props: z.object({\n        value: z.number(),\n        max: z.number().nullable(),\n      }),\n      description: \"Progress bar\",\n    },\n\n    RadioGroup: {\n      props: z.object({\n        value: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n        defaultValue: z.string().nullable(),\n      }),\n      description:\n        \"Radio button group. Use value with $bindState for two-way binding\",\n    },\n\n    Select: {\n      props: z.object({\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      description:\n        \"Dropdown select input. Use value with $bindState for two-way binding\",\n    },\n\n    Skeleton: {\n      props: z.object({\n        width: z.string().nullable(),\n        height: z.string().nullable(),\n      }),\n      description: \"Loading placeholder\",\n    },\n\n    Spinner: {\n      props: z.object({\n        size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      }),\n      description: \"Loading spinner\",\n    },\n\n    Switch: {\n      props: z.object({\n        label: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n      }),\n      description:\n        \"Toggle switch. Use checked with $bindState for two-way binding\",\n    },\n\n    Tabs: {\n      props: z.object({\n        defaultValue: z.string().nullable(),\n        tabs: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      slots: [\"default\"],\n      description: \"Tabbed content container\",\n    },\n\n    TabContent: {\n      props: z.object({\n        value: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Content for a specific tab\",\n    },\n\n    Textarea: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        rows: z.number().nullable(),\n      }),\n      description:\n        \"Multi-line text input. Use value with $bindState for two-way binding\",\n    },\n\n    Tooltip: {\n      props: z.object({\n        content: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Tooltip on hover\",\n    },\n\n    Table: {\n      props: z.object({\n        data: z.array(z.record(z.string(), z.unknown())),\n        columns: z.array(\n          z.object({\n            key: z.string(),\n            label: z.string(),\n          }),\n        ),\n        rowActions: z\n          .array(\n            z.object({\n              label: z.string(),\n              action: z.string(),\n              variant: z\n                .enum([\n                  \"default\",\n                  \"secondary\",\n                  \"destructive\",\n                  \"outline\",\n                  \"ghost\",\n                ])\n                .nullable(),\n            }),\n          )\n          .nullable(),\n        emptyMessage: z.string().nullable(),\n      }),\n      description:\n        \"Data table with optional row actions. Use { $state } on data to bind to an array of objects.\",\n      example: {\n        data: { $state: \"/customers/data\" },\n        columns: [\n          { key: \"name\", label: \"Name\" },\n          { key: \"email\", label: \"Email\" },\n        ],\n      },\n    },\n\n    // Typography\n    Heading: {\n      props: z.object({\n        text: z.string(),\n        level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      }),\n      description: \"Section heading\",\n      example: { text: \"Dashboard\", level: \"h1\" },\n    },\n\n    Text: {\n      props: z.object({\n        content: z.string(),\n        muted: z.boolean().nullable(),\n      }),\n      description: \"Text content\",\n      example: { content: \"Welcome back! Here is your overview.\" },\n    },\n\n    // Charts\n    BarChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        \"Bar chart visualization. Use { $state } on data to point to array of objects, xKey is the category/group field, yKey is the numeric value field. Use aggregate='count' to count items grouped by xKey (yKey becomes the count). For dates, xKey values are auto-formatted.\",\n    },\n\n    LineChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        \"Line chart visualization. Use { $state } on data to point to array of objects, xKey is the x-axis field, yKey is the numeric value field. Use aggregate='count' to count items grouped by xKey. For dates, xKey values are auto-formatted.\",\n    },\n  },\n\n  actions: {\n    // Customers - use these for customer-related widgets\n    viewCustomers: {\n      params: z.object({\n        status: z.enum([\"active\", \"inactive\"]).nullable(),\n        limit: z.number().nullable(),\n        sort: z.enum([\"newest\", \"oldest\"]).nullable(),\n      }),\n      description:\n        \"Fetch customers from GET /api/v1/customers. Supports ?limit=N&sort=newest|oldest. Data available at 'customers.data'\",\n    },\n    refreshCustomers: {\n      params: z.object({\n        limit: z.number().nullable(),\n        sort: z.enum([\"newest\", \"oldest\"]).nullable(),\n      }),\n      description:\n        \"Refresh customers from GET /api/v1/customers. Supports ?limit=N&sort=newest|oldest. Data available at 'customers.data'\",\n    },\n    createCustomer: {\n      params: z.object({\n        name: z.string(),\n        email: z.string(),\n        phone: z.string().nullable(),\n      }),\n      description: \"Create new customer via POST /api/v1/customers\",\n    },\n    deleteCustomer: {\n      params: z.object({\n        customerId: z.string(),\n      }),\n      description: \"Delete a customer via DELETE /api/v1/customers/:id\",\n    },\n\n    // Invoices - use these for invoice-related widgets\n    viewInvoices: {\n      params: z.object({\n        status: z.enum([\"draft\", \"sent\", \"paid\", \"overdue\"]).nullable(),\n      }),\n      description:\n        \"Fetch invoices from GET /api/v1/invoices. Data available at 'invoices.data'\",\n    },\n    refreshInvoices: {\n      params: z.object({}),\n      description:\n        \"Refresh invoices from GET /api/v1/invoices. Data available at 'invoices.data'\",\n    },\n    createInvoice: {\n      params: z.object({\n        customerId: z.string(),\n        dueDate: z.string(),\n      }),\n      description: \"Create new invoice via POST /api/v1/invoices\",\n    },\n    sendInvoice: {\n      params: z.object({\n        invoiceId: z.string(),\n      }),\n      description: \"Send invoice to customer\",\n    },\n    markInvoicePaid: {\n      params: z.object({\n        invoiceId: z.string(),\n      }),\n      description: \"Mark invoice as paid\",\n    },\n\n    // Expenses - use these for expense-related widgets\n    viewExpenses: {\n      params: z.object({\n        status: z.enum([\"pending\", \"approved\", \"rejected\"]).nullable(),\n      }),\n      description:\n        \"Fetch expenses from GET /api/v1/expenses. Data available at 'expenses.data'\",\n    },\n    refreshExpenses: {\n      params: z.object({}),\n      description:\n        \"Refresh expenses from GET /api/v1/expenses. Data available at 'expenses.data'\",\n    },\n    createExpense: {\n      params: z.object({\n        vendor: z.string(),\n        category: z.string(),\n        amount: z.number(),\n        description: z.string().nullable(),\n      }),\n      description: \"Create new expense via POST /api/v1/expenses\",\n    },\n    approveExpense: {\n      params: z.object({\n        expenseId: z.string(),\n      }),\n      description: \"Approve expense\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/dashboard/lib/render/registry.tsx",
    "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { findFormValue } from \"@json-render/core\";\nimport { useBoundProp, defineRegistry } from \"@json-render/react\";\nimport {\n  Bar,\n  BarChart as RechartsBarChart,\n  CartesianGrid,\n  Line,\n  LineChart as RechartsLineChart,\n  XAxis,\n} from \"recharts\";\nimport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\";\n\n// shadcn components\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Alert, AlertTitle, AlertDescription } from \"@/components/ui/alert\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n  Accordion,\n  AccordionItem,\n  AccordionTrigger,\n  AccordionContent,\n} from \"@/components/ui/accordion\";\nimport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableHead,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"@/components/ui/avatar\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n} from \"@/components/ui/dialog\";\nimport {\n  Drawer,\n  DrawerTrigger,\n  DrawerContent,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerDescription,\n} from \"@/components/ui/drawer\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationLink,\n} from \"@/components/ui/pagination\";\nimport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n} from \"@/components/ui/popover\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  Select,\n  SelectTrigger,\n  SelectValue,\n  SelectContent,\n  SelectItem,\n} from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from \"@/components/ui/tabs\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n  TooltipProvider,\n} from \"@/components/ui/tooltip\";\n\nimport { dashboardCatalog } from \"./catalog\";\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport const { registry, handlers, executeAction } = defineRegistry(\n  dashboardCatalog,\n  {\n    components: {\n      Stack: ({ props, children }) => {\n        const gapClass =\n          { sm: \"gap-2\", md: \"gap-4\", lg: \"gap-6\" }[props.gap ?? \"md\"] ??\n          \"gap-4\";\n        return (\n          <div\n            className={`flex ${props.direction === \"horizontal\" ? \"flex-row\" : \"flex-col\"} ${gapClass}`}\n          >\n            {children}\n          </div>\n        );\n      },\n\n      Accordion: ({ props, children }) => (\n        <Accordion type={props.type ?? \"single\"} collapsible>\n          {children}\n        </Accordion>\n      ),\n\n      AccordionItem: ({ props, children }) => (\n        <AccordionItem value={props.value}>\n          <AccordionTrigger>{props.title}</AccordionTrigger>\n          <AccordionContent>{children}</AccordionContent>\n        </AccordionItem>\n      ),\n\n      Button: ({ props, emit, loading }) => (\n        <Button\n          variant={props.variant ?? \"default\"}\n          disabled={loading || (props.disabled ?? false)}\n          onClick={() => emit(\"press\")}\n        >\n          {loading ? \"...\" : props.label}\n        </Button>\n      ),\n\n      Input: ({ props, bindings }) => {\n        const [value, setValue] = useBoundProp<string>(\n          props.value as string | undefined,\n          bindings?.value,\n        );\n        return (\n          <div className=\"flex flex-col gap-2\">\n            {props.label ? <Label>{props.label}</Label> : null}\n            <Input\n              type={props.type ?? \"text\"}\n              value={value ?? \"\"}\n              placeholder={props.placeholder ?? \"\"}\n              onChange={(e) => setValue(e.target.value)}\n            />\n          </div>\n        );\n      },\n\n      Form: ({ children, emit }) => (\n        <form\n          onSubmit={(e) => {\n            e.preventDefault();\n            emit(\"submit\");\n          }}\n          className=\"flex flex-col gap-4\"\n        >\n          {children}\n        </form>\n      ),\n\n      Badge: ({ props }) => (\n        <Badge variant={props.variant ?? \"default\"}>{props.text}</Badge>\n      ),\n\n      Alert: ({ props }) => (\n        <Alert variant={props.variant ?? \"default\"}>\n          <AlertTitle>{props.title}</AlertTitle>\n          {props.description ? (\n            <AlertDescription>{props.description}</AlertDescription>\n          ) : null}\n        </Alert>\n      ),\n\n      Separator: () => <Separator />,\n\n      Avatar: ({ props }) => (\n        <Avatar>\n          {props.src ? (\n            <AvatarImage src={props.src} alt={props.alt ?? \"\"} />\n          ) : null}\n          <AvatarFallback>{props.fallback}</AvatarFallback>\n        </Avatar>\n      ),\n\n      Checkbox: ({ props, bindings }) => {\n        const [checked, setChecked] = useBoundProp<boolean>(\n          props.checked as boolean | undefined,\n          bindings?.checked,\n        );\n        const isChecked = checked ?? props.defaultChecked ?? false;\n        return (\n          <div className=\"flex items-center gap-2\">\n            <Checkbox\n              id={bindings?.checked ?? \"checkbox\"}\n              checked={isChecked}\n              onCheckedChange={(value) => setChecked(value === true)}\n            />\n            {props.label ? (\n              <Label htmlFor={bindings?.checked ?? \"checkbox\"}>\n                {props.label}\n              </Label>\n            ) : null}\n          </div>\n        );\n      },\n\n      Dialog: ({ props, children }) => (\n        <Dialog>\n          <DialogTrigger asChild>\n            <Button variant=\"outline\">{props.trigger}</Button>\n          </DialogTrigger>\n          <DialogContent>\n            <DialogHeader>\n              <DialogTitle>{props.title}</DialogTitle>\n              {props.description ? (\n                <DialogDescription>{props.description}</DialogDescription>\n              ) : null}\n            </DialogHeader>\n            {children}\n          </DialogContent>\n        </Dialog>\n      ),\n\n      Drawer: ({ props, children }) => (\n        <Drawer>\n          <DrawerTrigger asChild>\n            <Button variant=\"outline\">{props.trigger}</Button>\n          </DrawerTrigger>\n          <DrawerContent>\n            <DrawerHeader>\n              <DrawerTitle>{props.title}</DrawerTitle>\n              {props.description ? (\n                <DrawerDescription>{props.description}</DrawerDescription>\n              ) : null}\n            </DrawerHeader>\n            <div className=\"p-4\">{children}</div>\n          </DrawerContent>\n        </Drawer>\n      ),\n\n      DropdownMenu: ({ props }) => (\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\">{props.trigger}</Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent>\n            {props.items.map((item, i) => (\n              <DropdownMenuItem key={i}>{item.label}</DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      ),\n\n      Label: ({ props }) => (\n        <Label htmlFor={props.htmlFor ?? undefined}>{props.text}</Label>\n      ),\n\n      Pagination: ({ props }) => {\n        const pages = Array.from({ length: props.totalPages }, (_, i) => i + 1);\n        return (\n          <Pagination>\n            <PaginationContent>\n              <PaginationItem>\n                <PaginationPrevious href=\"#\" />\n              </PaginationItem>\n              {pages.map((page) => (\n                <PaginationItem key={page}>\n                  <PaginationLink\n                    href=\"#\"\n                    isActive={page === props.currentPage}\n                  >\n                    {page}\n                  </PaginationLink>\n                </PaginationItem>\n              ))}\n              <PaginationItem>\n                <PaginationNext href=\"#\" />\n              </PaginationItem>\n            </PaginationContent>\n          </Pagination>\n        );\n      },\n\n      Popover: ({ props, children }) => (\n        <Popover>\n          <PopoverTrigger asChild>\n            <Button variant=\"outline\">{props.trigger}</Button>\n          </PopoverTrigger>\n          <PopoverContent>{children}</PopoverContent>\n        </Popover>\n      ),\n\n      Progress: ({ props }) => (\n        <Progress value={props.value} max={props.max ?? 100} />\n      ),\n\n      RadioGroup: ({ props, bindings }) => {\n        const [value, setValue] = useBoundProp<string>(\n          props.value as string | undefined,\n          bindings?.value,\n        );\n        const current = value ?? props.defaultValue ?? \"\";\n        return (\n          <RadioGroup value={current} onValueChange={(v) => setValue(v)}>\n            {props.options.map((option) => (\n              <div key={option.value} className=\"flex items-center gap-2\">\n                <RadioGroupItem value={option.value} id={option.value} />\n                <Label htmlFor={option.value}>{option.label}</Label>\n              </div>\n            ))}\n          </RadioGroup>\n        );\n      },\n\n      Select: ({ props, bindings }) => {\n        const [value, setValue] = useBoundProp<string>(\n          props.value as string | undefined,\n          bindings?.value,\n        );\n        return (\n          <Select value={value ?? \"\"} onValueChange={(v) => setValue(v)}>\n            <SelectTrigger>\n              <SelectValue placeholder={props.placeholder ?? \"Select...\"} />\n            </SelectTrigger>\n            <SelectContent>\n              {props.options.map((option) => (\n                <SelectItem key={option.value} value={option.value}>\n                  {option.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        );\n      },\n\n      Skeleton: ({ props }) => (\n        <Skeleton\n          className={`${props.width ?? \"w-full\"} ${props.height ?? \"h-4\"}`}\n        />\n      ),\n\n      Spinner: ({ props }) => {\n        const sizes = { sm: \"h-4 w-4\", md: \"h-6 w-6\", lg: \"h-8 w-8\" };\n        const size = sizes[props.size ?? \"md\"];\n        return (\n          <div\n            className={`${size} animate-spin rounded-full border-2 border-muted border-t-primary`}\n          />\n        );\n      },\n\n      Switch: ({ props, bindings }) => {\n        const [checked, setChecked] = useBoundProp<boolean>(\n          props.checked as boolean | undefined,\n          bindings?.checked,\n        );\n        const isChecked = checked ?? props.defaultChecked ?? false;\n        return (\n          <div className=\"flex items-center gap-2\">\n            <Switch\n              id={bindings?.checked ?? \"switch\"}\n              checked={isChecked}\n              onCheckedChange={(value) => setChecked(value)}\n            />\n            {props.label ? (\n              <Label htmlFor={bindings?.checked ?? \"switch\"}>\n                {props.label}\n              </Label>\n            ) : null}\n          </div>\n        );\n      },\n\n      Tabs: ({ props, children }) => (\n        <Tabs defaultValue={props.defaultValue ?? props.tabs[0]?.value}>\n          <TabsList>\n            {props.tabs.map((tab) => (\n              <TabsTrigger key={tab.value} value={tab.value}>\n                {tab.label}\n              </TabsTrigger>\n            ))}\n          </TabsList>\n          {children}\n        </Tabs>\n      ),\n\n      TabContent: ({ props, children }) => (\n        <TabsContent value={props.value}>{children}</TabsContent>\n      ),\n\n      Textarea: ({ props, bindings }) => {\n        const [value, setValue] = useBoundProp<string>(\n          props.value as string | undefined,\n          bindings?.value,\n        );\n        return (\n          <div className=\"flex flex-col gap-2\">\n            {props.label ? <Label>{props.label}</Label> : null}\n            <Textarea\n              value={value ?? \"\"}\n              placeholder={props.placeholder ?? \"\"}\n              rows={props.rows ?? 3}\n              onChange={(e) => setValue(e.target.value)}\n            />\n          </div>\n        );\n      },\n\n      Tooltip: ({ props, children }) => (\n        <TooltipProvider>\n          <Tooltip>\n            <TooltipTrigger asChild>{children}</TooltipTrigger>\n            <TooltipContent>{props.content}</TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      ),\n\n      // Heading is intentionally not rendered - widgets already have a title bar\n      Heading: () => null,\n\n      Text: ({ props }) => (\n        <p className={props.muted ? \"text-muted-foreground\" : \"\"}>\n          {props.content}\n        </p>\n      ),\n\n      Table: ({ props }) => {\n        const rawData = props.data;\n\n        const items: Array<Record<string, unknown>> = Array.isArray(rawData)\n          ? rawData\n          : Array.isArray((rawData as Record<string, unknown>)?.data)\n            ? ((rawData as Record<string, unknown>).data as Array<\n                Record<string, unknown>\n              >)\n            : Array.isArray((rawData as Record<string, unknown>)?.items)\n              ? ((rawData as Record<string, unknown>).items as Array<\n                  Record<string, unknown>\n                >)\n              : [];\n\n        if (items.length === 0) {\n          return (\n            <div className=\"text-center py-4 text-muted-foreground\">\n              {props.emptyMessage ?? \"No data\"}\n            </div>\n          );\n        }\n\n        const hasRowActions = props.rowActions && props.rowActions.length > 0;\n\n        return (\n          <Table>\n            <TableHeader>\n              <TableRow>\n                {props.columns.map((col) => (\n                  <TableHead key={col.key}>{col.label}</TableHead>\n                ))}\n                {hasRowActions ? <TableHead>Actions</TableHead> : null}\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {items.map((item, i) => (\n                <TableRow key={i}>\n                  {props.columns.map((col) => (\n                    <TableCell key={col.key}>\n                      {String(item[col.key] ?? \"\")}\n                    </TableCell>\n                  ))}\n                  {hasRowActions ? (\n                    <TableCell>\n                      <div className=\"flex gap-1\">\n                        {props.rowActions!.map((rowAction) => (\n                          <Button\n                            key={rowAction.action}\n                            variant={rowAction.variant ?? \"ghost\"}\n                            size=\"sm\"\n                          >\n                            {rowAction.label}\n                          </Button>\n                        ))}\n                      </div>\n                    </TableCell>\n                  ) : null}\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n        );\n      },\n\n      BarChart: ({ props }) => {\n        const rawData = props.data;\n\n        const rawItems: Array<Record<string, unknown>> = Array.isArray(rawData)\n          ? rawData\n          : Array.isArray((rawData as Record<string, unknown>)?.data)\n            ? ((rawData as Record<string, unknown>).data as Array<\n                Record<string, unknown>\n              >)\n            : [];\n\n        // Process and aggregate data\n        const { items, valueKey } = processChartData(\n          rawItems,\n          props.xKey,\n          props.yKey,\n          props.aggregate,\n        );\n\n        const chartColor = props.color ?? \"var(--chart-1)\";\n\n        const chartConfig = {\n          [valueKey]: {\n            label: valueKey,\n            color: chartColor,\n          },\n        } satisfies ChartConfig;\n\n        if (items.length === 0) {\n          return (\n            <div className=\"w-full\">\n              <div className=\"text-center py-4 text-muted-foreground\">\n                No data available\n              </div>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"w-full\">\n            <ChartContainer\n              config={chartConfig}\n              className=\"min-h-[200px] w-full\"\n              style={{ height: props.height ?? 300 }}\n            >\n              <RechartsBarChart accessibilityLayer data={items}>\n                <CartesianGrid vertical={false} />\n                <XAxis\n                  dataKey=\"label\"\n                  tickLine={false}\n                  tickMargin={10}\n                  axisLine={false}\n                />\n                <ChartTooltip content={<ChartTooltipContent />} />\n                <Bar\n                  dataKey={valueKey}\n                  fill={`var(--color-${valueKey})`}\n                  radius={4}\n                />\n              </RechartsBarChart>\n            </ChartContainer>\n          </div>\n        );\n      },\n\n      LineChart: ({ props }) => {\n        const rawData = props.data;\n\n        const rawItems: Array<Record<string, unknown>> = Array.isArray(rawData)\n          ? rawData\n          : Array.isArray((rawData as Record<string, unknown>)?.data)\n            ? ((rawData as Record<string, unknown>).data as Array<\n                Record<string, unknown>\n              >)\n            : [];\n\n        // Process and aggregate data\n        const { items, valueKey } = processChartData(\n          rawItems,\n          props.xKey,\n          props.yKey,\n          props.aggregate,\n        );\n\n        const chartColor = props.color ?? \"var(--chart-1)\";\n\n        const chartConfig = {\n          [valueKey]: {\n            label: valueKey,\n            color: chartColor,\n          },\n        } satisfies ChartConfig;\n\n        if (items.length === 0) {\n          return (\n            <div className=\"w-full\">\n              <div className=\"text-center py-4 text-muted-foreground\">\n                No data available\n              </div>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"w-full\">\n            <ChartContainer\n              config={chartConfig}\n              className=\"min-h-[200px] w-full\"\n              style={{ height: props.height ?? 300 }}\n            >\n              <RechartsLineChart accessibilityLayer data={items}>\n                <CartesianGrid vertical={false} />\n                <XAxis\n                  dataKey=\"label\"\n                  tickLine={false}\n                  tickMargin={10}\n                  axisLine={false}\n                />\n                <ChartTooltip content={<ChartTooltipContent />} />\n                <Line\n                  type=\"monotone\"\n                  dataKey={valueKey}\n                  stroke={`var(--color-${valueKey})`}\n                  strokeWidth={2}\n                  dot={false}\n                />\n              </RechartsLineChart>\n            </ChartContainer>\n          </div>\n        );\n      },\n    },\n\n    actions: {\n      viewCustomers: async (params, setState) => {\n        const queryParams = new URLSearchParams();\n        if (params?.limit) queryParams.set(\"limit\", String(params.limit));\n        if (params?.sort) queryParams.set(\"sort\", String(params.sort));\n        if (params?.status) queryParams.set(\"status\", String(params.status));\n        const url = `/api/v1/customers${queryParams.toString() ? `?${queryParams}` : \"\"}`;\n        const res = await fetch(url);\n        const customers = await res.json();\n        setState((prev) => ({ ...prev, customers }));\n      },\n\n      refreshCustomers: async (params, setState) => {\n        const queryParams = new URLSearchParams();\n        if (params?.limit) queryParams.set(\"limit\", String(params.limit));\n        if (params?.sort) queryParams.set(\"sort\", String(params.sort));\n        const url = `/api/v1/customers${queryParams.toString() ? `?${queryParams}` : \"\"}`;\n        const res = await fetch(url);\n        const customers = await res.json();\n        setState((prev) => ({ ...prev, customers }));\n      },\n\n      createCustomer: async (params, setState, state) => {\n        const name = findFormValue(\"name\", params, state) as string;\n        const email =\n          (findFormValue(\"email\", params, state) as string) ||\n          `${name?.toLowerCase().replace(/\\s+/g, \".\")}@example.com`;\n        const phone = findFormValue(\"phone\", params, state) as\n          | string\n          | undefined;\n\n        if (!name) {\n          toast.error(\"Customer name is required\");\n          return;\n        }\n\n        try {\n          const res = await fetch(\"/api/v1/customers\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify({ name, email, phone }),\n          });\n          const customer = await res.json();\n          if (res.ok) {\n            toast.success(`Customer \"${customer.name}\" created`);\n            const listRes = await fetch(\"/api/v1/customers\");\n            const customers = await listRes.json();\n            setState((prev) => ({ ...prev, customers }));\n          } else {\n            toast.error(customer.error || \"Failed to create customer\");\n          }\n        } catch (err) {\n          console.error(\"Failed to create customer:\", err);\n          toast.error(\"Failed to create customer\");\n        }\n      },\n\n      deleteCustomer: async (params, setState, state) => {\n        const customerId =\n          findFormValue(\"customerId\", params, state) ||\n          findFormValue(\"id\", params, state);\n        if (!customerId) {\n          toast.error(\"Customer ID required\");\n          return;\n        }\n        try {\n          const res = await fetch(`/api/v1/customers/${customerId}`, {\n            method: \"DELETE\",\n          });\n          if (res.ok) {\n            toast.success(\"Customer deleted\");\n            const listRes = await fetch(\"/api/v1/customers\");\n            const customers = await listRes.json();\n            setState((prev) => ({ ...prev, customers }));\n          } else {\n            const err = await res.json();\n            toast.error(err.error || \"Failed to delete customer\");\n          }\n        } catch (err) {\n          console.error(\"Failed to delete customer:\", err);\n          toast.error(\"Failed to delete customer\");\n        }\n      },\n\n      viewInvoices: async (params, setState) => {\n        const queryParams = new URLSearchParams();\n        if (params?.status) queryParams.set(\"status\", String(params.status));\n        const url = `/api/v1/invoices${queryParams.toString() ? `?${queryParams}` : \"\"}`;\n        const res = await fetch(url);\n        const invoices = await res.json();\n        setState((prev) => ({ ...prev, invoices }));\n      },\n\n      refreshInvoices: async (_params, setState) => {\n        const res = await fetch(\"/api/v1/invoices\");\n        const invoices = await res.json();\n        setState((prev) => ({ ...prev, invoices }));\n      },\n\n      createInvoice: async (params, setState) => {\n        if (!params?.customerId || !params?.dueDate) {\n          toast.error(\"Customer ID and due date required\");\n          return;\n        }\n        try {\n          const res = await fetch(\"/api/v1/invoices\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify(params),\n          });\n          const invoice = await res.json();\n          if (res.ok) {\n            toast.success(\"Invoice created\");\n            const listRes = await fetch(\"/api/v1/invoices\");\n            const invoices = await listRes.json();\n            setState((prev) => ({ ...prev, invoices }));\n          } else {\n            toast.error(invoice.error || \"Failed to create invoice\");\n          }\n        } catch (err) {\n          console.error(\"Failed to create invoice:\", err);\n          toast.error(\"Failed to create invoice\");\n        }\n      },\n\n      sendInvoice: async (params) => {\n        if (!params?.invoiceId) {\n          toast.error(\"Invoice ID required\");\n          return;\n        }\n        const res = await fetch(`/api/v1/invoices/${params.invoiceId}/send`, {\n          method: \"POST\",\n        });\n        const result = await res.json();\n        if (res.ok) {\n          toast.success(result.message || \"Invoice sent\");\n        } else {\n          toast.error(result.error || \"Failed to send invoice\");\n        }\n      },\n\n      markInvoicePaid: async (params) => {\n        if (!params?.invoiceId) {\n          toast.error(\"Invoice ID required\");\n          return;\n        }\n        const res = await fetch(\n          `/api/v1/invoices/${params.invoiceId}/mark-paid`,\n          { method: \"POST\" },\n        );\n        const result = await res.json();\n        if (res.ok) {\n          toast.success(result.message || \"Invoice marked paid\");\n        } else {\n          toast.error(result.error || \"Failed to mark invoice paid\");\n        }\n      },\n\n      viewExpenses: async (params, setState) => {\n        const queryParams = new URLSearchParams();\n        if (params?.status) queryParams.set(\"status\", String(params.status));\n        const url = `/api/v1/expenses${queryParams.toString() ? `?${queryParams}` : \"\"}`;\n        const res = await fetch(url);\n        const expenses = await res.json();\n        setState((prev) => ({ ...prev, expenses }));\n      },\n\n      refreshExpenses: async (_params, setState) => {\n        const res = await fetch(\"/api/v1/expenses\");\n        const expenses = await res.json();\n        setState((prev) => ({ ...prev, expenses }));\n      },\n\n      createExpense: async (params, setState) => {\n        if (\n          !params?.vendor ||\n          !params?.category ||\n          params?.amount === undefined\n        ) {\n          toast.error(\"Vendor, category, and amount required\");\n          return;\n        }\n        try {\n          const res = await fetch(\"/api/v1/expenses\", {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\" },\n            body: JSON.stringify(params),\n          });\n          const expense = await res.json();\n          if (res.ok) {\n            toast.success(\"Expense created\");\n            const listRes = await fetch(\"/api/v1/expenses\");\n            const expenses = await listRes.json();\n            setState((prev) => ({ ...prev, expenses }));\n          } else {\n            toast.error(expense.error || \"Failed to create expense\");\n          }\n        } catch (err) {\n          console.error(\"Failed to create expense:\", err);\n          toast.error(\"Failed to create expense\");\n        }\n      },\n\n      approveExpense: async (params) => {\n        if (!params?.expenseId) {\n          toast.error(\"Expense ID required\");\n          return;\n        }\n        const res = await fetch(\n          `/api/v1/expenses/${params.expenseId}/approve`,\n          { method: \"POST\" },\n        );\n        const result = await res.json();\n        if (res.ok) {\n          toast.success(result.message || \"Expense approved\");\n        } else {\n          toast.error(result.error || \"Failed to approve expense\");\n        }\n      },\n    },\n  },\n);\n\n// =============================================================================\n// Chart Helpers\n// =============================================================================\n\nfunction isISODate(value: unknown): boolean {\n  if (typeof value !== \"string\") return false;\n  return /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/.test(value);\n}\n\nfunction formatDateLabel(value: string): string {\n  const date = new Date(value);\n  if (isNaN(date.getTime())) return value;\n  return date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"2-digit\" });\n}\n\nfunction processChartData(\n  items: Array<Record<string, unknown>>,\n  xKey: string,\n  yKey: string,\n  aggregate: \"sum\" | \"count\" | \"avg\" | null | undefined,\n): { items: Array<Record<string, unknown>>; valueKey: string } {\n  if (items.length === 0) {\n    return { items: [], valueKey: yKey };\n  }\n\n  const firstXValue = items[0]?.[xKey];\n  const isDateKey = isISODate(firstXValue);\n\n  if (!aggregate) {\n    const formatted = items.map((item) => {\n      const xValue = item[xKey];\n      return {\n        ...item,\n        label:\n          isDateKey && typeof xValue === \"string\"\n            ? formatDateLabel(xValue)\n            : String(xValue ?? \"\"),\n      };\n    });\n    return { items: formatted, valueKey: yKey };\n  }\n\n  const groups = new Map<string, Array<Record<string, unknown>>>();\n\n  for (const item of items) {\n    const xValue = item[xKey];\n    let groupKey: string;\n\n    if (isDateKey && typeof xValue === \"string\") {\n      groupKey = xValue.split(\"T\")[0] ?? xValue;\n    } else {\n      groupKey = String(xValue ?? \"unknown\");\n    }\n\n    const group = groups.get(groupKey) ?? [];\n    group.push(item);\n    groups.set(groupKey, group);\n  }\n\n  const valueKey = aggregate === \"count\" ? \"count\" : yKey;\n  const aggregated: Array<Record<string, unknown>> = [];\n  const sortedKeys = Array.from(groups.keys()).sort();\n\n  for (const key of sortedKeys) {\n    const group = groups.get(key)!;\n    let value: number;\n\n    if (aggregate === \"count\") {\n      value = group.length;\n    } else if (aggregate === \"sum\") {\n      value = group.reduce((sum, item) => {\n        const v = item[yKey];\n        return sum + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n      }, 0);\n    } else {\n      const sum = group.reduce((s, item) => {\n        const v = item[yKey];\n        return s + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n      }, 0);\n      value = group.length > 0 ? sum / group.length : 0;\n    }\n\n    let label: string;\n    if (isDateKey) {\n      const date = new Date(key);\n      label = date.toLocaleDateString(\"en-US\", {\n        month: \"short\",\n        day: \"2-digit\",\n      });\n    } else {\n      label = key;\n    }\n\n    aggregated.push({\n      label,\n      [valueKey]: value,\n      _groupKey: key,\n    });\n  }\n\n  return { items: aggregated, valueKey };\n}\n\n// =============================================================================\n// Fallback Component\n// =============================================================================\n\nexport function Fallback({ type }: { type: string }) {\n  return (\n    <div className=\"p-4 border border-dashed rounded-lg text-muted-foreground text-sm\">\n      Unknown component: {type}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/lib/render/renderer.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useRef, type ReactNode } from \"react\";\nimport {\n  Renderer,\n  type ComponentRenderer,\n  type Spec,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n} from \"@json-render/react\";\n\nimport { registry, Fallback, handlers as createHandlers } from \"./registry\";\n\n// =============================================================================\n// DashboardRenderer\n// =============================================================================\n\ntype SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\ninterface DashboardRendererProps {\n  spec: Spec | null;\n  state?: Record<string, unknown>;\n  setState?: SetState;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  loading?: boolean;\n}\n\n// Fallback component for unknown types\nconst fallback: ComponentRenderer = ({ element }) => (\n  <Fallback type={element.type} />\n);\n\nexport function DashboardRenderer({\n  spec,\n  state = {},\n  setState,\n  onStateChange,\n  loading,\n}: DashboardRendererProps): ReactNode {\n  // Use refs so action handlers always see the latest state/setState\n  const stateRef = useRef(state);\n  const setStateRef = useRef(setState);\n  stateRef.current = state;\n  setStateRef.current = setState;\n\n  // Create ActionProvider-compatible handlers using getters so they\n  // always read the latest state/setState from refs\n  const actionHandlers = useMemo(\n    () =>\n      createHandlers(\n        () => setStateRef.current,\n        () => stateRef.current,\n      ),\n    [],\n  );\n\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={state} onStateChange={onStateChange}>\n      <VisibilityProvider>\n        <ActionProvider handlers={actionHandlers}>\n          <Renderer\n            spec={spec}\n            registry={registry}\n            fallback={fallback}\n            loading={loading}\n          />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/dashboard/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/dashboard/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/dashboard/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  transpilePackages: ['@json-render/core', '@json-render/react'],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/dashboard/package.json",
    "content": "{\n  \"name\": \"example-dashboard\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless dashboard-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint --max-warnings 0\",\n    \"db:clear\": \"tsx scripts/clear.ts\",\n    \"db:generate\": \"drizzle-kit generate\",\n    \"db:migrate\": \"drizzle-kit migrate\",\n    \"db:push\": \"drizzle-kit push\",\n    \"db:seed\": \"tsx scripts/seed.ts\",\n    \"db:studio\": \"drizzle-kit studio\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.13\",\n    \"@dnd-kit/core\": \"6.3.1\",\n    \"@dnd-kit/sortable\": \"10.0.0\",\n    \"@dnd-kit/utilities\": \"3.2.2\",\n    \"@json-render/codegen\": \"workspace:*\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"ai\": \"^6.0.33\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"drizzle-orm\": \"^0.45.1\",\n    \"lucide-react\": \"^0.562.0\",\n    \"next\": \"16.1.1\",\n    \"next-themes\": \"^0.4.6\",\n    \"postgres\": \"^3.4.8\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\",\n    \"recharts\": \"^2.15.4\",\n    \"shiki\": \"^3.0.0\",\n    \"sonner\": \"^2.0.7\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"vaul\": \"^1.1.2\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"dotenv\": \"^17.2.3\",\n    \"drizzle-kit\": \"^0.31.8\",\n    \"eslint\": \"^9.39.1\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tsx\": \"^4.21.0\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/dashboard/postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "examples/dashboard/scripts/clear.ts",
    "content": "import \"dotenv/config\";\nimport { clearDatabase } from \"../lib/db/store\";\n\nasync function main() {\n  console.log(\"Clearing database...\");\n  const result = await clearDatabase();\n  console.log(result.message);\n  process.exit(0);\n}\n\nmain().catch((err) => {\n  console.error(\"Error clearing database:\", err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "examples/dashboard/scripts/seed.ts",
    "content": "import \"dotenv/config\";\nimport { resetDatabase } from \"../lib/db/store\";\n\nasync function main() {\n  console.log(\"Seeding database...\");\n  const result = await resetDatabase();\n  console.log(result.message);\n  process.exit(0);\n}\n\nmain().catch((err) => {\n  console.error(\"Error seeding database:\", err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "examples/dashboard/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/image/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/image/CHANGELOG.md",
    "content": "# example-image\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/image@0.14.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/image@0.14.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/image@0.13.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/image@0.12.1\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/image@0.12.0\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/image@0.11.0\n"
  },
  {
    "path": "examples/image/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { buildUserPrompt, type Spec } from \"@json-render/core\";\nimport { imageCatalog } from \"@/lib/catalog\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 60;\n\nconst SYSTEM_PROMPT = imageCatalog.prompt();\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt, startingSpec } = (await req.json()) as {\n    prompt: string;\n    startingSpec?: Spec | null;\n  };\n\n  if (!prompt || typeof prompt !== \"string\") {\n    return Response.json({ error: \"prompt is required\" }, { status: 400 });\n  }\n\n  const userPrompt = buildUserPrompt({\n    prompt,\n    currentSpec: startingSpec,\n  });\n\n  const result = streamText({\n    model: process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    prompt: userPrompt,\n    temperature: 0.7,\n  });\n\n  return result.toTextStreamResponse();\n}\n"
  },
  {
    "path": "examples/image/app/api/image/route.ts",
    "content": "import { renderToPng } from \"@json-render/image/render\";\nimport { examples } from \"@/lib/examples\";\nimport type { Spec } from \"@json-render/core\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nlet fontCache: ArrayBuffer | null = null;\n\nasync function loadFont(): Promise<ArrayBuffer> {\n  if (fontCache) return fontCache;\n  const fontPath = join(\n    process.cwd(),\n    \"node_modules\",\n    \"geist\",\n    \"dist\",\n    \"fonts\",\n    \"geist-sans\",\n    \"Geist-Regular.ttf\",\n  );\n  const buffer = await readFile(fontPath);\n  fontCache = buffer.buffer.slice(\n    buffer.byteOffset,\n    buffer.byteOffset + buffer.byteLength,\n  );\n  return fontCache;\n}\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const name = searchParams.get(\"name\") ?? \"og-image\";\n  const download = searchParams.get(\"download\") === \"1\";\n\n  const example = examples.find((e) => e.name === name);\n  if (!example) {\n    return new Response(\"Example not found\", { status: 404 });\n  }\n\n  return imageResponse(example.spec, name, download);\n}\n\nexport async function POST(req: Request) {\n  const { spec, download, filename } = (await req.json()) as {\n    spec: Spec;\n    download?: boolean;\n    filename?: string;\n  };\n\n  if (!spec || !spec.root || !spec.elements) {\n    return new Response(\"Invalid spec\", { status: 400 });\n  }\n\n  return imageResponse(spec, filename ?? \"image\", download ?? false);\n}\n\nasync function imageResponse(spec: Spec, name: string, download: boolean) {\n  const fontData = await loadFont();\n  const fonts = [\n    {\n      name: \"Geist Sans\",\n      data: fontData,\n      weight: 400 as const,\n      style: \"normal\" as const,\n    },\n  ];\n\n  const png = await renderToPng(spec, { fonts });\n\n  const disposition = download\n    ? `attachment; filename=\"${name}.png\"`\n    : `inline; filename=\"${name}.png\"`;\n\n  return new Response(Buffer.from(png), {\n    headers: {\n      \"Content-Type\": \"image/png\",\n      \"Content-Disposition\": disposition,\n      \"Cache-Control\": \"no-store\",\n    },\n  });\n}\n"
  },
  {
    "path": "examples/image/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n    --radius-2xl: calc(var(--radius) + 8px);\n    --radius-3xl: calc(var(--radius) + 12px);\n    --radius-4xl: calc(var(--radius) + 16px);\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n    --radius: 0.625rem;\n    --background: oklch(1 0 0);\n    --foreground: oklch(0.145 0 0);\n    --card: oklch(1 0 0);\n    --card-foreground: oklch(0.145 0 0);\n    --popover: oklch(1 0 0);\n    --popover-foreground: oklch(0.145 0 0);\n    --primary: oklch(0.205 0 0);\n    --primary-foreground: oklch(0.985 0 0);\n    --secondary: oklch(0.97 0 0);\n    --secondary-foreground: oklch(0.205 0 0);\n    --muted: oklch(0.97 0 0);\n    --muted-foreground: oklch(0.556 0 0);\n    --accent: oklch(0.97 0 0);\n    --accent-foreground: oklch(0.205 0 0);\n    --destructive: oklch(0.577 0.245 27.325);\n    --border: oklch(0.922 0 0);\n    --input: oklch(0.922 0 0);\n    --ring: oklch(0.708 0 0);\n    --chart-1: oklch(0.646 0.222 41.116);\n    --chart-2: oklch(0.6 0.118 184.704);\n    --chart-3: oklch(0.398 0.07 227.392);\n    --chart-4: oklch(0.828 0.189 84.429);\n    --chart-5: oklch(0.769 0.188 70.08);\n    --sidebar: oklch(0.985 0 0);\n    --sidebar-foreground: oklch(0.145 0 0);\n    --sidebar-primary: oklch(0.205 0 0);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.97 0 0);\n    --sidebar-accent-foreground: oklch(0.205 0 0);\n    --sidebar-border: oklch(0.922 0 0);\n    --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n    --background: oklch(0.145 0 0);\n    --foreground: oklch(0.985 0 0);\n    --card: oklch(0.205 0 0);\n    --card-foreground: oklch(0.985 0 0);\n    --popover: oklch(0.205 0 0);\n    --popover-foreground: oklch(0.985 0 0);\n    --primary: oklch(0.922 0 0);\n    --primary-foreground: oklch(0.205 0 0);\n    --secondary: oklch(0.269 0 0);\n    --secondary-foreground: oklch(0.985 0 0);\n    --muted: oklch(0.269 0 0);\n    --muted-foreground: oklch(0.708 0 0);\n    --accent: oklch(0.269 0 0);\n    --accent-foreground: oklch(0.985 0 0);\n    --destructive: oklch(0.704 0.191 22.216);\n    --border: oklch(1 0 0 / 10%);\n    --input: oklch(1 0 0 / 15%);\n    --ring: oklch(0.556 0 0);\n    --chart-1: oklch(0.488 0.243 264.376);\n    --chart-2: oklch(0.696 0.17 162.48);\n    --chart-3: oklch(0.769 0.188 70.08);\n    --chart-4: oklch(0.627 0.265 303.9);\n    --chart-5: oklch(0.645 0.246 16.439);\n    --sidebar: oklch(0.205 0 0);\n    --sidebar-foreground: oklch(0.985 0 0);\n    --sidebar-primary: oklch(0.488 0.243 264.376);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.269 0 0);\n    --sidebar-accent-foreground: oklch(0.985 0 0);\n    --sidebar-border: oklch(1 0 0 / 10%);\n    --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "examples/image/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst geist = Geist({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"json-render Image Example\",\n  description: \"Generate images from JSON specs with @json-render/image\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body className={geist.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/image/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { examples } from \"@/lib/examples\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport type { Spec } from \"@json-render/core\";\nimport { cn } from \"@/lib/utils\";\n\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/ui/sheet\";\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n} from \"@/components/ui/resizable\";\nimport { ImageIcon, Download, Loader2, ArrowRight, Square } from \"lucide-react\";\n\ntype Mode = \"scratch\" | \"example\";\ntype MobileView = \"json\" | \"preview\";\n\ninterface Selection {\n  mode: Mode;\n  exampleName?: string;\n}\n\nconst IMAGE_REFRESH_INTERVAL_MS = 3000;\n\nfunction CopyButton({ text }: { text: string }) {\n  const [copied, setCopied] = useState(false);\n\n  return (\n    <button\n      onClick={() => {\n        navigator.clipboard.writeText(text);\n        setCopied(true);\n        setTimeout(() => setCopied(false), 1500);\n      }}\n      className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono\"\n    >\n      {copied ? \"copied\" : \"copy\"}\n    </button>\n  );\n}\n\nfunction isRenderableSpec(spec: Spec | null): spec is Spec {\n  if (!spec?.root || !spec.elements) return false;\n  const root = spec.elements[spec.root];\n  return root?.type === \"Frame\";\n}\n\nexport default function Page() {\n  const [selection, setSelection] = useState<Selection>({\n    mode: \"example\",\n    exampleName: examples[0]!.name,\n  });\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n  const [generatedSpec, setGeneratedSpec] = useState<Spec | null>(null);\n  const [imageUrl, setImageUrl] = useState<string | null>(null);\n  const [error, setError] = useState<string | null>(null);\n  const [mobileView, setMobileView] = useState<MobileView>(\"preview\");\n  const [examplesSheetOpen, setExamplesSheetOpen] = useState(false);\n  const [refreshing, setRefreshing] = useState(false);\n  const imageUrlRef = useRef<string | null>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const mobileInputRef = useRef<HTMLTextAreaElement>(null);\n  const abortRef = useRef<AbortController | null>(null);\n  const codeScrollRef = useRef<HTMLDivElement>(null);\n  const mobileCodeScrollRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!generating) return;\n    codeScrollRef.current?.scrollTo({\n      top: codeScrollRef.current.scrollHeight,\n    });\n    mobileCodeScrollRef.current?.scrollTo({\n      top: mobileCodeScrollRef.current.scrollHeight,\n    });\n  }, [generating, generatedSpec]);\n\n  const currentExample =\n    selection.mode === \"example\"\n      ? examples.find((e) => e.name === selection.exampleName)\n      : null;\n\n  const activeSpec = generatedSpec ?? currentExample?.spec ?? null;\n\n  const exampleImageUrl =\n    selection.mode === \"example\" && !generatedSpec\n      ? `/api/image?name=${selection.exampleName}`\n      : null;\n\n  const displayImageUrl = imageUrl ?? exampleImageUrl;\n\n  useEffect(() => {\n    inputRef.current?.focus();\n  }, [selection.mode, selection.exampleName]);\n\n  const fetchImageBlob = useCallback(\n    async (spec: Spec, signal?: AbortSignal) => {\n      const res = await fetch(\"/api/image\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ spec }),\n        signal,\n      });\n      if (!res.ok) throw new Error(\"Failed to generate image\");\n      const blob = await res.blob();\n      const url = URL.createObjectURL(blob);\n\n      const prev = imageUrlRef.current;\n      imageUrlRef.current = url;\n      setImageUrl(url);\n\n      if (prev) URL.revokeObjectURL(prev);\n    },\n    [],\n  );\n\n  const lastRefreshSpec = useRef<string>(\"\");\n  const generatedSpecRef = useRef<Spec | null>(null);\n  generatedSpecRef.current = generatedSpec;\n\n  useEffect(() => {\n    if (!generating) return;\n\n    const interval = setInterval(() => {\n      const spec = generatedSpecRef.current;\n      if (!spec) return;\n\n      const specKey = JSON.stringify(spec);\n      if (specKey === lastRefreshSpec.current) return;\n      if (!isRenderableSpec(spec)) return;\n\n      lastRefreshSpec.current = specKey;\n      setRefreshing(true);\n      fetchImageBlob(spec)\n        .catch(() => {})\n        .finally(() => setRefreshing(false));\n    }, IMAGE_REFRESH_INTERVAL_MS);\n\n    return () => clearInterval(interval);\n  }, [generating, fetchImageBlob]);\n\n  const handleGenerate = useCallback(async () => {\n    if (!prompt.trim()) return;\n\n    abortRef.current?.abort();\n    const controller = new AbortController();\n    abortRef.current = controller;\n\n    setGenerating(true);\n    setError(null);\n    lastRefreshSpec.current = \"\";\n\n    try {\n      const startingSpec =\n        selection.mode === \"example\" && currentExample\n          ? currentExample.spec\n          : null;\n\n      const res = await fetch(\"/api/generate\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt: prompt.trim(), startingSpec }),\n        signal: controller.signal,\n      });\n      if (!res.ok) throw new Error(\"Generation failed\");\n\n      const reader = res.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n      const compiler = createSpecStreamCompiler<Spec>(\n        startingSpec ? { ...startingSpec } : {},\n      );\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        const chunk = decoder.decode(value, { stream: true });\n        const { result, newPatches } = compiler.push(chunk);\n        if (newPatches.length > 0) setGeneratedSpec(result);\n      }\n\n      const finalSpec = compiler.getResult();\n      setGeneratedSpec(finalSpec);\n      setGenerating(false);\n\n      await fetchImageBlob(finalSpec);\n    } catch (e) {\n      if (controller.signal.aborted) return;\n      setError(e instanceof Error ? e.message : \"Something went wrong\");\n      setGenerating(false);\n    }\n  }, [prompt, selection, currentExample, fetchImageBlob]);\n\n  const handleStop = useCallback(() => {\n    abortRef.current?.abort();\n    setGenerating(false);\n\n    if (isRenderableSpec(generatedSpec)) {\n      fetchImageBlob(generatedSpec).catch(() => {});\n    }\n  }, [generatedSpec, fetchImageBlob]);\n\n  const select = (next: Selection) => {\n    abortRef.current?.abort();\n    setSelection(next);\n    setGeneratedSpec(null);\n    setImageUrl(null);\n    setError(null);\n    setPrompt(\"\");\n    setGenerating(false);\n    setExamplesSheetOpen(false);\n  };\n\n  const handleDownload = async () => {\n    if (!activeSpec) return;\n    if (generatedSpec) {\n      const res = await fetch(\"/api/image\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ spec: generatedSpec, download: true }),\n      });\n      const blob = await res.blob();\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = \"image.png\";\n      a.click();\n      URL.revokeObjectURL(url);\n    } else if (selection.mode === \"example\") {\n      window.open(\n        `/api/image?name=${selection.exampleName}&download=1`,\n        \"_blank\",\n      );\n    }\n  };\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleGenerate();\n      }\n    },\n    [handleGenerate],\n  );\n\n  const jsonCode = activeSpec\n    ? JSON.stringify(activeSpec, null, 2)\n    : \"// select an example or generate an image\";\n\n  // ---------------------------------------------------------------------------\n  // Pane: Chat / Examples\n  // ---------------------------------------------------------------------------\n  const chatPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-2\">\n        <ImageIcon className=\"h-3.5 w-3.5 text-muted-foreground\" />\n        <span className=\"text-xs font-mono text-muted-foreground\">\n          json-render / image\n        </span>\n      </div>\n\n      <ScrollArea className=\"flex-1\">\n        <div className=\"p-2 space-y-1\">\n          <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n            start\n          </p>\n          <button\n            onClick={() => select({ mode: \"scratch\" })}\n            className={cn(\n              \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n              selection.mode === \"scratch\"\n                ? \"bg-muted text-foreground\"\n                : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n            )}\n          >\n            <span className=\"font-medium\">From scratch</span>\n          </button>\n\n          <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n            examples\n          </p>\n          {examples.map((ex) => (\n            <button\n              key={ex.name}\n              onClick={() => select({ mode: \"example\", exampleName: ex.name })}\n              className={cn(\n                \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                selection.mode === \"example\" &&\n                  selection.exampleName === ex.name\n                  ? \"bg-muted text-foreground\"\n                  : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n              )}\n            >\n              <span className=\"font-medium\">{ex.label}</span>\n              <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                {ex.description}\n              </p>\n            </button>\n          ))}\n        </div>\n      </ScrollArea>\n\n      <div\n        className=\"border-t border-border p-3 cursor-text\"\n        onMouseDown={(e) => {\n          const target = e.target as HTMLElement;\n          if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n            e.preventDefault();\n            inputRef.current?.focus();\n          }\n        }}\n      >\n        {error && (\n          <div className=\"mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive\">\n            {error}\n          </div>\n        )}\n        <textarea\n          ref={inputRef}\n          value={prompt}\n          onChange={(e) => setPrompt(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={\n            selection.mode === \"scratch\"\n              ? \"Describe the image you want...\"\n              : `Modify the ${currentExample?.label ?? \"example\"}...`\n          }\n          className=\"w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50\"\n          rows={2}\n          autoFocus\n        />\n        <div className=\"flex justify-between items-center mt-2\">\n          <span className=\"text-[11px] text-muted-foreground\">\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </span>\n          {generating ? (\n            <button\n              onClick={handleStop}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n              aria-label=\"Stop\"\n            >\n              <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n            </button>\n          ) : (\n            <button\n              onClick={handleGenerate}\n              disabled={!prompt.trim()}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n              aria-label=\"Generate\"\n            >\n              <ArrowRight className=\"h-3.5 w-3.5\" />\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: JSON Spec\n  // ---------------------------------------------------------------------------\n  const codePane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">json</span>\n        {generating && (\n          <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && <CopyButton text={jsonCode} />}\n      </div>\n      <div ref={codeScrollRef} className=\"flex-1 overflow-auto\">\n        <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n          {jsonCode}\n        </pre>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: Image Preview\n  // ---------------------------------------------------------------------------\n  const previewPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">preview</span>\n        {(generating || refreshing) && (\n          <Loader2 className=\"h-3.5 w-3.5 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && (\n          <button\n            onClick={handleDownload}\n            className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1\"\n          >\n            <Download className=\"h-3 w-3\" />\n            download\n          </button>\n        )}\n      </div>\n      <div className=\"flex-1 relative bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center p-4\">\n        {displayImageUrl ? (\n          <img\n            key={displayImageUrl}\n            src={displayImageUrl}\n            alt=\"Generated image\"\n            className=\"max-w-full max-h-full object-contain rounded shadow-lg\"\n          />\n        ) : (\n          <div className=\"flex flex-col items-center justify-center gap-2 text-neutral-400\">\n            <ImageIcon className=\"h-10 w-10\" />\n            <p className=\"text-sm\">\n              {selection.mode === \"scratch\"\n                ? \"Enter a prompt to generate an image\"\n                : \"Select an example to preview\"}\n            </p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Render\n  // ---------------------------------------------------------------------------\n  return (\n    <div className=\"h-dvh flex flex-col\">\n      {/* Desktop: 3-pane resizable layout */}\n      <div className=\"hidden lg:flex flex-1 min-h-0\">\n        <ResizablePanelGroup className=\"flex-1\">\n          <ResizablePanel defaultSize={25} minSize={15}>\n            {chatPane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={35} minSize={20}>\n            {codePane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={40} minSize={20}>\n            {previewPane}\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      </div>\n\n      {/* Mobile: toolbar + content + prompt */}\n      <div className=\"flex lg:hidden flex-col flex-1 min-h-0\">\n        <div className=\"border-b border-border h-10 flex items-center px-1\">\n          <button\n            onClick={() => setExamplesSheetOpen(true)}\n            className=\"px-3 h-full text-xs font-mono text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5\"\n          >\n            <ImageIcon className=\"h-3.5 w-3.5\" />\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </button>\n          <div className=\"flex-1\" />\n          <button\n            onClick={() => setMobileView(\"json\")}\n            className={cn(\n              \"px-3 h-full text-xs font-mono transition-colors\",\n              mobileView === \"json\"\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            json\n          </button>\n          <button\n            onClick={() => setMobileView(\"preview\")}\n            className={cn(\n              \"px-3 h-full text-xs font-mono transition-colors\",\n              mobileView === \"preview\"\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            preview\n          </button>\n          {activeSpec && (\n            <button\n              onClick={handleDownload}\n              className=\"px-3 h-full text-xs text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <Download className=\"h-3.5 w-3.5\" />\n            </button>\n          )}\n        </div>\n\n        <div className=\"flex-1 min-h-0\">\n          {mobileView === \"json\" ? (\n            <div ref={mobileCodeScrollRef} className=\"h-full overflow-auto\">\n              <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n                {jsonCode}\n              </pre>\n            </div>\n          ) : (\n            <div className=\"h-full bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center p-4\">\n              {displayImageUrl ? (\n                <img\n                  key={displayImageUrl}\n                  src={displayImageUrl}\n                  alt=\"Generated image\"\n                  className=\"max-w-full max-h-full object-contain rounded shadow-lg\"\n                />\n              ) : (\n                <div className=\"flex flex-col items-center justify-center gap-2 text-neutral-400\">\n                  <ImageIcon className=\"h-10 w-10\" />\n                  <p className=\"text-sm\">\n                    {selection.mode === \"scratch\"\n                      ? \"Enter a prompt to generate an image\"\n                      : \"Select an example\"}\n                  </p>\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n\n        <div\n          className=\"border-t border-border p-3 cursor-text\"\n          onMouseDown={(e) => {\n            const target = e.target as HTMLElement;\n            if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n              e.preventDefault();\n              mobileInputRef.current?.focus();\n            }\n          }}\n        >\n          {error && (\n            <div className=\"mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive\">\n              {error}\n            </div>\n          )}\n          <textarea\n            ref={mobileInputRef}\n            value={prompt}\n            onChange={(e) => setPrompt(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder={\n              selection.mode === \"scratch\"\n                ? \"Describe the image you want...\"\n                : `Modify the ${currentExample?.label ?? \"example\"}...`\n            }\n            className=\"w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50\"\n            rows={2}\n          />\n          <div className=\"flex justify-end mt-2\">\n            {generating ? (\n              <button\n                onClick={handleStop}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n                aria-label=\"Stop\"\n              >\n                <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n              </button>\n            ) : (\n              <button\n                onClick={handleGenerate}\n                disabled={!prompt.trim()}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n                aria-label=\"Generate\"\n              >\n                <ArrowRight className=\"h-3.5 w-3.5\" />\n              </button>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <Sheet open={examplesSheetOpen} onOpenChange={setExamplesSheetOpen}>\n        <SheetContent side=\"left\" className=\"w-72 p-0\">\n          <SheetTitle className=\"sr-only\">Examples</SheetTitle>\n          <ScrollArea className=\"h-full\">\n            <div className=\"p-2 space-y-1\">\n              <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n                start\n              </p>\n              <button\n                onClick={() => select({ mode: \"scratch\" })}\n                className={cn(\n                  \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                  selection.mode === \"scratch\"\n                    ? \"bg-muted text-foreground\"\n                    : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                )}\n              >\n                <span className=\"font-medium\">From scratch</span>\n              </button>\n\n              <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n                examples\n              </p>\n              {examples.map((ex) => (\n                <button\n                  key={ex.name}\n                  onClick={() =>\n                    select({ mode: \"example\", exampleName: ex.name })\n                  }\n                  className={cn(\n                    \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                    selection.mode === \"example\" &&\n                      selection.exampleName === ex.name\n                      ? \"bg-muted text-foreground\"\n                      : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                  )}\n                >\n                  <span className=\"font-medium\">{ex.label}</span>\n                  <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                    {ex.description}\n                  </p>\n                </button>\n              ))}\n            </div>\n          </ScrollArea>\n        </SheetContent>\n      </Sheet>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/image/components/ui/resizable.tsx",
    "content": "\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ResizablePanelGroup({\n  className,\n  ...props\n}: ResizablePrimitive.GroupProps) {\n  return (\n    <ResizablePrimitive.Group\n      data-slot=\"resizable-panel-group\"\n      className={cn(\n        \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {\n  return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({\n  withHandle,\n  className,\n  ...props\n}: ResizablePrimitive.SeparatorProps & {\n  withHandle?: boolean;\n}) {\n  return (\n    <ResizablePrimitive.Separator\n      data-slot=\"resizable-handle\"\n      className={cn(\n        \"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      {withHandle && (\n        <div className=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n          <GripVerticalIcon className=\"size-2.5\" />\n        </div>\n      )}\n    </ResizablePrimitive.Separator>\n  );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"
  },
  {
    "path": "examples/image/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ScrollArea({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"relative\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        data-slot=\"scroll-area-viewport\"\n        className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  );\n}\n\nfunction ScrollBar({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      className={cn(\n        \"flex touch-none p-px transition-colors select-none\",\n        orientation === \"vertical\" &&\n          \"h-full w-2.5 border-l border-l-transparent\",\n        orientation === \"horizontal\" &&\n          \"h-2.5 flex-col border-t border-t-transparent\",\n        className,\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-border relative flex-1 rounded-full\"\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  );\n}\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "examples/image/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  showCloseButton?: boolean;\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n            <XIcon className=\"size-4\" />\n            <span className=\"sr-only\">Close</span>\n          </SheetPrimitive.Close>\n        )}\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "examples/image/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rtl\": false,\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "examples/image/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      \"react/prop-types\": \"off\",\n      \"@next/next/no-img-element\": \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "examples/image/lib/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/image/server\";\nimport { standardComponentDefinitions } from \"@json-render/image/catalog\";\n\nexport const imageCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n});\n"
  },
  {
    "path": "examples/image/lib/examples.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport interface Example {\n  name: string;\n  label: string;\n  description: string;\n  spec: Spec;\n}\n\nexport const examples: Example[] = [\n  // ==========================================================================\n  // OG Image\n  // ==========================================================================\n  {\n    name: \"og-image\",\n    label: \"OG Image\",\n    description: \"Social sharing card for Open Graph (1200x630)\",\n    spec: {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: {\n            width: 1200,\n            height: 630,\n            backgroundColor: \"#0f172a\",\n            padding: 60,\n            flexDirection: \"column\",\n            justifyContent: \"space-between\",\n          },\n          children: [\"top-row\", \"content\", \"bottom-row\"],\n        },\n        \"top-row\": {\n          type: \"Row\",\n          props: { gap: 12, alignItems: \"center\" },\n          children: [\"logo-dot\", \"brand\"],\n        },\n        \"logo-dot\": {\n          type: \"Box\",\n          props: {\n            width: 32,\n            height: 32,\n            backgroundColor: \"#6366f1\",\n            borderRadius: 16,\n          },\n          children: [],\n        },\n        brand: {\n          type: \"Text\",\n          props: { text: \"acme.dev\", fontSize: 24, color: \"#94a3b8\" },\n          children: [],\n        },\n        content: {\n          type: \"Column\",\n          props: { gap: 16 },\n          children: [\"title\", \"subtitle\"],\n        },\n        title: {\n          type: \"Heading\",\n          props: {\n            text: \"Build faster with the modern developer platform\",\n            level: \"h1\",\n            color: \"#f8fafc\",\n            letterSpacing: \"-0.02em\",\n            lineHeight: 1.1,\n          },\n          children: [],\n        },\n        subtitle: {\n          type: \"Text\",\n          props: {\n            text: \"Ship production-ready apps in minutes, not months.\",\n            fontSize: 24,\n            color: \"#94a3b8\",\n            lineHeight: 1.4,\n          },\n          children: [],\n        },\n        \"bottom-row\": {\n          type: \"Row\",\n          props: { gap: 8, alignItems: \"center\" },\n          children: [\"url\"],\n        },\n        url: {\n          type: \"Text\",\n          props: {\n            text: \"acme.dev/platform\",\n            fontSize: 18,\n            color: \"#6366f1\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n      },\n    },\n  },\n\n  // ==========================================================================\n  // Blog Post Card\n  // ==========================================================================\n  {\n    name: \"blog-card\",\n    label: \"Blog Post Card\",\n    description: \"Social card for a blog post with author info\",\n    spec: {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: {\n            width: 1200,\n            height: 630,\n            backgroundColor: \"#ffffff\",\n            padding: 60,\n            flexDirection: \"column\",\n            justifyContent: \"space-between\",\n          },\n          children: [\"header\", \"body\", \"footer\"],\n        },\n        header: {\n          type: \"Row\",\n          props: { gap: 10, alignItems: \"center\" },\n          children: [\"category-badge\"],\n        },\n        \"category-badge\": {\n          type: \"Box\",\n          props: {\n            backgroundColor: \"#dbeafe\",\n            borderRadius: 20,\n            paddingTop: 6,\n            paddingBottom: 6,\n            paddingLeft: 16,\n            paddingRight: 16,\n          },\n          children: [\"category-text\"],\n        },\n        \"category-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Engineering\",\n            fontSize: 16,\n            color: \"#1d4ed8\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        body: {\n          type: \"Column\",\n          props: { gap: 20 },\n          children: [\"blog-title\", \"blog-excerpt\"],\n        },\n        \"blog-title\": {\n          type: \"Heading\",\n          props: {\n            text: \"How We Reduced API Latency by 90% with Edge Computing\",\n            level: \"h1\",\n            color: \"#0f172a\",\n            letterSpacing: \"-0.02em\",\n            lineHeight: 1.15,\n          },\n          children: [],\n        },\n        \"blog-excerpt\": {\n          type: \"Text\",\n          props: {\n            text: \"A deep dive into our migration from centralized servers to edge functions, and the performance gains we achieved along the way.\",\n            fontSize: 22,\n            color: \"#64748b\",\n            lineHeight: 1.5,\n          },\n          children: [],\n        },\n        footer: {\n          type: \"Row\",\n          props: { gap: 16, alignItems: \"center\" },\n          children: [\"author-avatar\", \"author-info\"],\n        },\n        \"author-avatar\": {\n          type: \"Box\",\n          props: {\n            width: 48,\n            height: 48,\n            backgroundColor: \"#e2e8f0\",\n            borderRadius: 24,\n          },\n          children: [],\n        },\n        \"author-info\": {\n          type: \"Column\",\n          props: { gap: 2 },\n          children: [\"author-name\", \"author-date\"],\n        },\n        \"author-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Sarah Chen\",\n            fontSize: 18,\n            color: \"#0f172a\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"author-date\": {\n          type: \"Text\",\n          props: {\n            text: \"February 15, 2026\",\n            fontSize: 16,\n            color: \"#94a3b8\",\n          },\n          children: [],\n        },\n      },\n    },\n  },\n\n  // ==========================================================================\n  // Event Banner\n  // ==========================================================================\n  {\n    name: \"event-banner\",\n    label: \"Event Banner\",\n    description: \"Wide promotional banner for an event (1920x1080)\",\n    spec: {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: {\n            width: 1920,\n            height: 1080,\n            backgroundColor: \"#18181b\",\n            padding: 100,\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          },\n          children: [\n            \"badge-row\",\n            \"spacer-1\",\n            \"event-title\",\n            \"spacer-2\",\n            \"details-row\",\n            \"spacer-3\",\n            \"cta\",\n          ],\n        },\n        \"badge-row\": {\n          type: \"Row\",\n          props: { alignItems: \"center\", justifyContent: \"center\" },\n          children: [\"live-badge\"],\n        },\n        \"live-badge\": {\n          type: \"Box\",\n          props: {\n            backgroundColor: \"#dc2626\",\n            borderRadius: 24,\n            paddingTop: 8,\n            paddingBottom: 8,\n            paddingLeft: 24,\n            paddingRight: 24,\n          },\n          children: [\"live-text\"],\n        },\n        \"live-text\": {\n          type: \"Text\",\n          props: {\n            text: \"LIVE EVENT\",\n            fontSize: 18,\n            color: \"#ffffff\",\n            fontWeight: \"bold\",\n            letterSpacing: \"0.1em\",\n          },\n          children: [],\n        },\n        \"spacer-1\": {\n          type: \"Spacer\",\n          props: { height: 40 },\n          children: [],\n        },\n        \"event-title\": {\n          type: \"Heading\",\n          props: {\n            text: \"DevConf 2026\",\n            level: \"h1\",\n            color: \"#ffffff\",\n            align: \"center\",\n            letterSpacing: \"-0.03em\",\n            lineHeight: 1.0,\n          },\n          children: [],\n        },\n        \"spacer-2\": {\n          type: \"Spacer\",\n          props: { height: 32 },\n          children: [],\n        },\n        \"details-row\": {\n          type: \"Row\",\n          props: {\n            gap: 40,\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          },\n          children: [\"date-text\", \"dot-sep\", \"location-text\"],\n        },\n        \"date-text\": {\n          type: \"Text\",\n          props: {\n            text: \"March 15-17, 2026\",\n            fontSize: 28,\n            color: \"#a1a1aa\",\n          },\n          children: [],\n        },\n        \"dot-sep\": {\n          type: \"Text\",\n          props: { text: \"/\", fontSize: 28, color: \"#52525b\" },\n          children: [],\n        },\n        \"location-text\": {\n          type: \"Text\",\n          props: {\n            text: \"San Francisco, CA\",\n            fontSize: 28,\n            color: \"#a1a1aa\",\n          },\n          children: [],\n        },\n        \"spacer-3\": {\n          type: \"Spacer\",\n          props: { height: 48 },\n          children: [],\n        },\n        cta: {\n          type: \"Box\",\n          props: {\n            backgroundColor: \"#6366f1\",\n            borderRadius: 12,\n            paddingTop: 16,\n            paddingBottom: 16,\n            paddingLeft: 48,\n            paddingRight: 48,\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          },\n          children: [\"cta-text\"],\n        },\n        \"cta-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Register Now\",\n            fontSize: 24,\n            color: \"#ffffff\",\n            fontWeight: \"bold\",\n            letterSpacing: \"0.02em\",\n          },\n          children: [],\n        },\n      },\n    },\n  },\n\n  // ==========================================================================\n  // Social Square\n  // ==========================================================================\n  {\n    name: \"social-square\",\n    label: \"Social Square\",\n    description: \"Square format card for Instagram or social media (1080x1080)\",\n    spec: {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: {\n            width: 1080,\n            height: 1080,\n            backgroundColor: \"#faf5ff\",\n            padding: 80,\n            flexDirection: \"column\",\n            justifyContent: \"space-between\",\n          },\n          children: [\"top-section\", \"quote-section\", \"bottom-section\"],\n        },\n        \"top-section\": {\n          type: \"Row\",\n          props: {\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n          },\n          children: [\"quote-icon\", \"slide-number\"],\n        },\n        \"quote-icon\": {\n          type: \"Heading\",\n          props: { text: '\"', level: \"h1\", color: \"#9333ea\" },\n          children: [],\n        },\n        \"slide-number\": {\n          type: \"Text\",\n          props: {\n            text: \"01 / 05\",\n            fontSize: 18,\n            color: \"#a855f7\",\n            fontWeight: \"bold\",\n            letterSpacing: \"0.1em\",\n          },\n          children: [],\n        },\n        \"quote-section\": {\n          type: \"Column\",\n          props: { gap: 24 },\n          children: [\"quote-text\"],\n        },\n        \"quote-text\": {\n          type: \"Heading\",\n          props: {\n            text: \"The best way to predict the future is to create it.\",\n            level: \"h2\",\n            color: \"#1e1b4b\",\n            letterSpacing: \"-0.01em\",\n            lineHeight: 1.3,\n          },\n          children: [],\n        },\n        \"bottom-section\": {\n          type: \"Column\",\n          props: { gap: 4 },\n          children: [\"author-name-sq\", \"author-title\"],\n        },\n        \"author-name-sq\": {\n          type: \"Text\",\n          props: {\n            text: \"Peter Drucker\",\n            fontSize: 22,\n            color: \"#1e1b4b\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"author-title\": {\n          type: \"Text\",\n          props: {\n            text: \"Management Consultant & Author\",\n            fontSize: 18,\n            color: \"#7c3aed\",\n          },\n          children: [],\n        },\n      },\n    },\n  },\n\n  // ==========================================================================\n  // Bar Graph\n  // ==========================================================================\n  {\n    name: \"bar-graph\",\n    label: \"Bar Graph\",\n    description: \"Data visualization bar chart (1200x630)\",\n    spec: {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: {\n            width: 1200,\n            height: 630,\n            backgroundColor: \"#ffffff\",\n            padding: 48,\n            flexDirection: \"column\",\n          },\n          children: [\"header\", \"chart-area\"],\n        },\n        header: {\n          type: \"Column\",\n          props: { gap: 4 },\n          children: [\"chart-title\", \"chart-subtitle\"],\n        },\n        \"chart-title\": {\n          type: \"Heading\",\n          props: {\n            text: \"Monthly Revenue\",\n            level: \"h2\",\n            color: \"#0f172a\",\n            letterSpacing: \"-0.02em\",\n            lineHeight: 1.2,\n          },\n          children: [],\n        },\n        \"chart-subtitle\": {\n          type: \"Text\",\n          props: {\n            text: \"Q3-Q4 2025 (in thousands USD)\",\n            fontSize: 18,\n            color: \"#94a3b8\",\n          },\n          children: [],\n        },\n        \"chart-area\": {\n          type: \"Row\",\n          props: {\n            gap: 0,\n            alignItems: \"flex-end\",\n            justifyContent: \"space-between\",\n            flex: 1,\n          },\n          children: [\n            \"bar-jul\",\n            \"bar-aug\",\n            \"bar-sep\",\n            \"bar-oct\",\n            \"bar-nov\",\n            \"bar-dec\",\n          ],\n        },\n\n        \"bar-jul\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-jul\", \"rect-jul\", \"lbl-jul\"],\n        },\n        \"val-jul\": {\n          type: \"Text\",\n          props: {\n            text: \"$42k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-jul\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 168,\n            backgroundColor: \"#6366f1\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-jul\": {\n          type: \"Text\",\n          props: {\n            text: \"Jul\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n\n        \"bar-aug\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-aug\", \"rect-aug\", \"lbl-aug\"],\n        },\n        \"val-aug\": {\n          type: \"Text\",\n          props: {\n            text: \"$58k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-aug\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 232,\n            backgroundColor: \"#6366f1\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-aug\": {\n          type: \"Text\",\n          props: {\n            text: \"Aug\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n\n        \"bar-sep\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-sep\", \"rect-sep\", \"lbl-sep\"],\n        },\n        \"val-sep\": {\n          type: \"Text\",\n          props: {\n            text: \"$51k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-sep\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 204,\n            backgroundColor: \"#6366f1\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-sep\": {\n          type: \"Text\",\n          props: {\n            text: \"Sep\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n\n        \"bar-oct\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-oct\", \"rect-oct\", \"lbl-oct\"],\n        },\n        \"val-oct\": {\n          type: \"Text\",\n          props: {\n            text: \"$73k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-oct\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 292,\n            backgroundColor: \"#818cf8\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-oct\": {\n          type: \"Text\",\n          props: {\n            text: \"Oct\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n\n        \"bar-nov\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-nov\", \"rect-nov\", \"lbl-nov\"],\n        },\n        \"val-nov\": {\n          type: \"Text\",\n          props: {\n            text: \"$85k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-nov\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 340,\n            backgroundColor: \"#818cf8\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-nov\": {\n          type: \"Text\",\n          props: {\n            text: \"Nov\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n\n        \"bar-dec\": {\n          type: \"Column\",\n          props: {\n            gap: 8,\n            alignItems: \"center\",\n            justifyContent: \"flex-end\",\n            flex: 1,\n          },\n          children: [\"val-dec\", \"rect-dec\", \"lbl-dec\"],\n        },\n        \"val-dec\": {\n          type: \"Text\",\n          props: {\n            text: \"$97k\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n            fontWeight: \"bold\",\n          },\n          children: [],\n        },\n        \"rect-dec\": {\n          type: \"Box\",\n          props: {\n            width: 80,\n            height: 388,\n            backgroundColor: \"#4f46e5\",\n            borderRadius: 8,\n          },\n          children: [],\n        },\n        \"lbl-dec\": {\n          type: \"Text\",\n          props: {\n            text: \"Dec\",\n            fontSize: 16,\n            color: \"#64748b\",\n            align: \"center\",\n          },\n          children: [],\n        },\n      },\n    },\n  },\n];\n"
  },
  {
    "path": "examples/image/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:image:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:image:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/image/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/image/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/image/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  serverExternalPackages: [\"@resvg/resvg-js\", \"satori\"],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/image/package.json",
    "content": "{\n  \"name\": \"example-image\",\n  \"version\": \"0.1.6\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"portless image-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"check-types\": \"tsc --noEmit\",\n    \"lint\": \"eslint --max-warnings 0\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/image\": \"workspace:*\",\n    \"@resvg/resvg-js\": \"^2.6.2\",\n    \"satori\": \"^0.19.2\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"ai\": \"6.0.103\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"geist\": \"^1.7.0\",\n    \"lucide-react\": \"^0.575.0\",\n    \"next\": \"16.1.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-resizable-panels\": \"^4.4.1\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"eslint\": \"^9.39.1\",\n    \"shadcn\": \"^3.8.5\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/image/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/image/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/mcp/CHANGELOG.md",
    "content": "# example-mcp\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/mcp@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/shadcn@0.14.1\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/mcp@0.14.0\n  - @json-render/react@0.14.0\n  - @json-render/shadcn@0.14.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/mcp@0.13.0\n  - @json-render/react@0.13.0\n  - @json-render/shadcn@0.13.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/mcp@0.12.1\n  - @json-render/react@0.12.1\n  - @json-render/shadcn@0.12.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/mcp@0.12.0\n  - @json-render/react@0.12.0\n  - @json-render/shadcn@0.12.0\n"
  },
  {
    "path": "examples/mcp/README.md",
    "content": "# MCP App Example\n\nA json-render MCP App that serves interactive shadcn/ui-based UIs directly inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## Setup\n\n```bash\npnpm install\npnpm build\n```\n\n## Usage\n\n### With Cursor\n\nAdd to `.cursor/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"json-render\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"examples/mcp/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### With Claude Desktop\n\nAdd to `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"json-render\": {\n      \"command\": \"npx\",\n      \"args\": [\"tsx\", \"/absolute/path/to/examples/mcp/server.ts\", \"--stdio\"]\n    }\n  }\n}\n```\n\n### HTTP Transport\n\n```bash\npnpm start\n# Server listens on http://localhost:3001/mcp\n```\n\n### Stdio Transport\n\n```bash\npnpm start:stdio\n```\n\n## How It Works\n\n1. The Vite build bundles the React app (with shadcn components and `useJsonRenderApp` hook) into a single self-contained HTML file\n2. The MCP server registers a `render-ui` tool with the catalog prompt as its description\n3. When the LLM calls the tool, it generates a json-render spec constrained to the catalog\n4. The host renders the bundled HTML in a sandboxed iframe\n5. The iframe receives the spec via the MCP Apps protocol and renders it with json-render\n"
  },
  {
    "path": "examples/mcp/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;\" />\n  <title>json-render MCP App</title>\n</head>\n<body>\n  <div id=\"root\"></div>\n  <script type=\"module\" src=\"/src/main.tsx\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "examples/mcp/package.json",
    "content": "{\n  \"name\": \"example-mcp\",\n  \"version\": \"0.1.5\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"start\": \"tsx server.ts\",\n    \"start:stdio\": \"tsx server.ts --stdio\",\n    \"dev\": \"tsx watch server.ts\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/mcp\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/shadcn\": \"workspace:*\",\n    \"@modelcontextprotocol/ext-apps\": \"^1.2.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.27.1\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@tailwindcss/vite\": \"^4.2.1\",\n    \"@vitejs/plugin-react\": \"^5.1.4\",\n    \"tailwindcss\": \"^4.2.1\",\n    \"tsx\": \"^4.21.0\",\n    \"typescript\": \"^5.7.2\",\n    \"vite\": \"^6.3.5\",\n    \"vite-plugin-singlefile\": \"^2.3.0\"\n  }\n}\n"
  },
  {
    "path": "examples/mcp/server.ts",
    "content": "import { createMcpApp } from \"@json-render/mcp\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { createMcpExpressApp } from \"@modelcontextprotocol/sdk/server/express.js\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { catalog } from \"./src/catalog.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nfunction loadHtml(): string {\n  const htmlPath = path.join(__dirname, \"dist\", \"index.html\");\n  if (!fs.existsSync(htmlPath)) {\n    throw new Error(\n      `Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,\n    );\n  }\n  return fs.readFileSync(htmlPath, \"utf-8\");\n}\n\nasync function startStdio() {\n  const html = loadHtml();\n  const server = await createMcpApp({\n    name: \"json-render Example\",\n    version: \"1.0.0\",\n    catalog,\n    html,\n  });\n  await server.connect(new StdioServerTransport());\n}\n\nasync function startHttp() {\n  const html = loadHtml();\n  const port = parseInt(process.env.PORT ?? \"3001\", 10);\n\n  const expressApp = createMcpExpressApp({ host: \"0.0.0.0\" });\n\n  expressApp.all(\"/mcp\", async (req, res) => {\n    const server = await createMcpApp({\n      name: \"json-render Example\",\n      version: \"1.0.0\",\n      catalog,\n      html,\n    });\n\n    const transport = new StreamableHTTPServerTransport({\n      sessionIdGenerator: undefined,\n    });\n\n    res.on(\"close\", () => {\n      transport.close().catch(() => {});\n      server.close().catch(() => {});\n    });\n\n    try {\n      await server.connect(transport);\n      await transport.handleRequest(req, res, req.body);\n    } catch (error) {\n      console.error(\"MCP error:\", error);\n      if (!res.headersSent) {\n        res.status(500).json({\n          jsonrpc: \"2.0\",\n          error: { code: -32603, message: \"Internal server error\" },\n          id: null,\n        });\n      }\n    }\n  });\n\n  expressApp.listen(port, () => {\n    console.log(`MCP server listening on http://localhost:${port}/mcp`);\n  });\n}\n\nif (process.argv.includes(\"--stdio\")) {\n  startStdio().catch((e) => {\n    console.error(e);\n    process.exit(1);\n  });\n} else {\n  startHttp().catch((e) => {\n    console.error(e);\n    process.exit(1);\n  });\n}\n"
  },
  {
    "path": "examples/mcp/src/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    ...shadcnComponentDefinitions,\n  },\n  actions: {},\n});\n"
  },
  {
    "path": "examples/mcp/src/globals.css",
    "content": "@import \"tailwindcss\";\n@source \"../../../packages/shadcn/src\";\n@source \"../../../packages/react/src\";\n\n@custom-variant dark (&:is([data-theme=\"dark\"] *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n}\n\n[data-theme=\"dark\"] {\n  --background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));\n  --foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));\n  --card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));\n  --card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));\n  --popover: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));\n  --popover-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));\n  --primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));\n  --primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));\n  --secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));\n  --secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));\n  --muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));\n  --muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));\n  --accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));\n  --accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));\n  --destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));\n  --border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));\n  --input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));\n  --ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));\n}\n\n* {\n  box-sizing: border-box;\n  border-color: var(--color-border);\n}\n\nhtml, body, #root {\n  width: 100%;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n}\n\nbody {\n  background-color: var(--color-background);\n  color: var(--color-foreground);\n  font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply text-foreground;\n    background-color: var(--color-background) !important;\n  }\n}\n\nbutton {\n  cursor: pointer;\n}\n"
  },
  {
    "path": "examples/mcp/src/main.tsx",
    "content": "import \"./globals.css\";\nimport {\n  Component,\n  useState,\n  useEffect,\n  useCallback,\n  type ReactNode,\n} from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { App as McpApp } from \"@modelcontextprotocol/ext-apps\";\nimport { JSONUIProvider, Renderer, defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\nimport type { Spec } from \"@json-render/core\";\nimport { catalog } from \"./catalog\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    ...shadcnComponents,\n    Avatar: ({ props }: { props: Record<string, unknown> }) => {\n      const name = (props.name as string) || (props.alt as string) || \"?\";\n      const src = props.src as string | undefined;\n      const initials = name\n        .split(\" \")\n        .map((n) => n[0])\n        .join(\"\")\n        .slice(0, 2)\n        .toUpperCase();\n      const size = props.size === \"lg\" ? 48 : props.size === \"sm\" ? 32 : 40;\n      const [imgFailed, setImgFailed] = useState(false);\n      const onError = useCallback(() => setImgFailed(true), []);\n      const showImg = src && !imgFailed;\n\n      return (\n        <div\n          style={{\n            width: size,\n            height: size,\n            borderRadius: \"50%\",\n            overflow: \"hidden\",\n            backgroundColor: \"var(--color-background-tertiary, #27272a)\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            flexShrink: 0,\n          }}\n        >\n          {showImg ? (\n            <img\n              src={src}\n              alt={name}\n              referrerPolicy=\"no-referrer\"\n              crossOrigin=\"anonymous\"\n              width={size}\n              height={size}\n              onError={onError}\n              style={{ objectFit: \"cover\", width: size, height: size }}\n            />\n          ) : (\n            <span\n              style={{\n                fontSize: size * 0.4,\n                color: \"var(--color-text-secondary, #a1a1aa)\",\n              }}\n            >\n              {initials}\n            </span>\n          )}\n        </div>\n      );\n    },\n  },\n});\n\nclass ErrorBoundary extends Component<\n  { children: ReactNode },\n  { error: Error | null }\n> {\n  state = { error: null as Error | null };\n  static getDerivedStateFromError(error: Error) {\n    return { error };\n  }\n  render() {\n    if (this.state.error) {\n      return (\n        <div\n          style={{\n            padding: 16,\n            color: \"#dc2626\",\n            fontFamily: \"monospace\",\n            fontSize: 13,\n          }}\n        >\n          Error: {this.state.error.message}\n        </div>\n      );\n    }\n    return this.props.children;\n  }\n}\n\nfunction forceFullWidth(spec: Spec): Spec {\n  if (!spec.elements) return spec;\n  const elements = { ...spec.elements };\n  for (const [key, el] of Object.entries(elements)) {\n    if (el.type === \"Card\" && el.props) {\n      elements[key] = {\n        ...el,\n        props: { ...el.props, maxWidth: \"full\", centered: false },\n      };\n    }\n  }\n  return { ...spec, elements };\n}\n\nfunction parseSpec(\n  result: { content?: Array<{ type: string; text?: string }> } | undefined,\n): Spec | null {\n  const text = result?.content?.find((c) => c.type === \"text\")?.text;\n  if (!text) return null;\n  try {\n    return forceFullWidth(JSON.parse(text) as Spec);\n  } catch {\n    return null;\n  }\n}\n\nfunction applyHostContext(ctx: {\n  theme?: string;\n  styles?: { variables?: Record<string, string> };\n}) {\n  if (ctx.theme) {\n    document.documentElement.setAttribute(\"data-theme\", ctx.theme);\n    document.documentElement.style.colorScheme = ctx.theme;\n  }\n  if (ctx.styles?.variables) {\n    const root = document.documentElement;\n    for (const [key, value] of Object.entries(ctx.styles.variables)) {\n      root.style.setProperty(key, value);\n    }\n  }\n}\n\nfunction McpAppView() {\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [error, setError] = useState<string | null>(null);\n\n  useEffect(() => {\n    let specReceived = false;\n\n    function handleSpec(s: Spec) {\n      if (specReceived) return;\n      specReceived = true;\n      setSpec(s);\n    }\n\n    function onMessage(event: MessageEvent) {\n      if (specReceived) return;\n      const data = event.data as Record<string, unknown> | undefined;\n      if (!data || typeof data !== \"object\") return;\n      const method = data.method as string | undefined;\n      const params = data.params as Record<string, unknown> | undefined;\n\n      // Cursor sends the spec via tool-input\n      if (method === \"ui/notifications/tool-input\" && params?.arguments) {\n        const args = params.arguments as Record<string, unknown>;\n        const rawSpec = args.spec;\n        if (\n          rawSpec &&\n          typeof rawSpec === \"object\" &&\n          \"root\" in rawSpec &&\n          \"elements\" in rawSpec\n        ) {\n          handleSpec(forceFullWidth(rawSpec as Spec));\n        }\n      }\n    }\n    window.addEventListener(\"message\", onMessage);\n\n    const app = new McpApp({ name: \"json-render\", version: \"1.0.0\" });\n\n    app.ontoolresult = (result) => {\n      const parsed = parseSpec(\n        result as { content?: Array<{ type: string; text?: string }> },\n      );\n      if (parsed) handleSpec(parsed);\n    };\n\n    app.onhostcontextchanged = (ctx) =>\n      applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);\n\n    app.onerror = (err: unknown) => {\n      setError(err instanceof Error ? err.message : String(err));\n    };\n\n    app\n      .connect()\n      .then(() => {\n        const ctx = app.getHostContext?.();\n        if (ctx)\n          applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);\n\n        // Fallback: if host didn't provide theme, detect via media query\n        if (!ctx || !(ctx as Record<string, unknown>).theme) {\n          const prefersDark = window.matchMedia(\n            \"(prefers-color-scheme: dark)\",\n          ).matches;\n          document.documentElement.setAttribute(\n            \"data-theme\",\n            prefersDark ? \"dark\" : \"light\",\n          );\n          document.documentElement.style.colorScheme = prefersDark\n            ? \"dark\"\n            : \"light\";\n        }\n      })\n      .catch((err: unknown) =>\n        setError(err instanceof Error ? err.message : String(err)),\n      );\n\n    return () => {\n      window.removeEventListener(\"message\", onMessage);\n      app.close().catch(() => {});\n    };\n  }, []);\n\n  if (error) {\n    return (\n      <div\n        style={{\n          padding: 16,\n          color: \"#dc2626\",\n          fontFamily: \"monospace\",\n          fontSize: 13,\n        }}\n      >\n        {error}\n      </div>\n    );\n  }\n\n  if (!spec) {\n    return (\n      <div\n        style={{\n          padding: 16,\n          color: \"#6b7280\",\n          fontFamily: \"sans-serif\",\n          fontSize: 14,\n        }}\n      >\n        Loading...\n      </div>\n    );\n  }\n\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <div className=\"w-full\">\n        <Renderer spec={spec} registry={registry} />\n      </div>\n    </JSONUIProvider>\n  );\n}\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <ErrorBoundary>\n    <McpAppView />\n  </ErrorBoundary>,\n);\n"
  },
  {
    "path": "examples/mcp/src/mcp-app-view.tsx",
    "content": "import { JSONUIProvider, Renderer } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { useJsonRenderApp } from \"@json-render/mcp/app\";\nimport { catalog } from \"./catalog\";\nimport { useState, useEffect } from \"react\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    ...shadcnComponents,\n  },\n});\n\nconst debugStyle = {\n  padding: 12,\n  fontFamily: \"monospace\",\n  fontSize: 12,\n  color: \"#000\",\n  background: \"#fffbe6\",\n  border: \"1px solid #faad14\",\n  borderRadius: 4,\n  margin: 8,\n  whiteSpace: \"pre-wrap\" as const,\n  wordBreak: \"break-all\" as const,\n};\n\nexport function McpAppView() {\n  const [logs, setLogs] = useState<string[]>([\"App mounted\"]);\n\n  const addLog = (msg: string) => {\n    setLogs((prev) => [\n      ...prev,\n      `${new Date().toISOString().slice(11, 23)} ${msg}`,\n    ]);\n  };\n\n  const { spec, loading, connected, connecting, error } = useJsonRenderApp({\n    name: \"json-render-mcp-example\",\n    version: \"1.0.0\",\n  });\n\n  useEffect(() => {\n    addLog(\n      `State: connecting=${connecting} connected=${connected} error=${error?.message ?? \"none\"} loading=${loading} spec=${spec ? \"yes\" : \"null\"}`,\n    );\n  }, [connecting, connected, error, loading, spec]);\n\n  useEffect(() => {\n    if (spec) {\n      addLog(\n        `Spec received: root=${spec.root}, elements=${Object.keys(spec.elements ?? {}).join(\",\")}`,\n      );\n    }\n  }, [spec]);\n\n  if (error) {\n    return (\n      <div style={debugStyle}>\n        <div style={{ color: \"#ef4444\", fontWeight: \"bold\" }}>\n          Connection error: {error.message}\n        </div>\n        <div style={{ marginTop: 8 }}>{logs.join(\"\\n\")}</div>\n      </div>\n    );\n  }\n\n  if (!spec) {\n    return (\n      <div style={debugStyle}>\n        <div>\n          {connecting\n            ? \"Connecting to host...\"\n            : loading\n              ? \"Waiting for UI spec...\"\n              : \"No spec received.\"}\n        </div>\n        <div style={{ marginTop: 8 }}>{logs.join(\"\\n\")}</div>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n        <Renderer spec={spec} registry={registry} loading={loading} />\n      </JSONUIProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/mcp/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"src\", \"server.ts\"]\n}\n"
  },
  {
    "path": "examples/mcp/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport { viteSingleFile } from \"vite-plugin-singlefile\";\nimport tailwindcss from \"@tailwindcss/vite\";\n\nexport default defineConfig({\n  plugins: [react(), tailwindcss(), viteSingleFile()],\n  resolve: {\n    dedupe: [\"react\", \"react-dom\"],\n  },\n  build: {\n    outDir: \"dist\",\n    emptyOutDir: false,\n    rollupOptions: {\n      input: \"index.html\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/no-ai/CHANGELOG.md",
    "content": "# example-no-ai\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/shadcn@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n  - @json-render/shadcn@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n  - @json-render/shadcn@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n  - @json-render/shadcn@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react@0.12.0\n  - @json-render/shadcn@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n  - @json-render/shadcn@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n  - @json-render/shadcn@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/shadcn@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n  - @json-render/shadcn@0.9.0\n"
  },
  {
    "path": "examples/no-ai/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@source \"../../../packages/shadcn/src/**/*.tsx\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --radius-3xl: calc(var(--radius) + 12px);\n  --radius-4xl: calc(var(--radius) + 16px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbutton {\n  cursor: pointer;\n}\n"
  },
  {
    "path": "examples/no-ai/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Geist, Geist_Mono } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst geistSans = Geist({\n  variable: \"--font-geist-sans\",\n  subsets: [\"latin\"],\n});\n\nconst geistMono = Geist_Mono({\n  variable: \"--font-geist-mono\",\n  subsets: [\"latin\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"json-render No-AI Example\",\n  description: \"Static JSON specs rendered with json-render -- no AI required\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}\n      >\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/no-ai/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useEffect, useCallback, type ReactNode } from \"react\";\nimport ConfettiExplosion from \"react-confetti-explosion\";\nimport { JSONUIProvider, Renderer } from \"@json-render/react\";\nimport type { Spec } from \"@json-render/core\";\nimport {\n  registry,\n  actionHandlers,\n  computedFunctions,\n  onConfetti,\n} from \"@/lib/render/registry\";\nimport { examples } from \"@/lib/examples\";\n\nfunction SpecRenderer({ spec }: { spec: Spec }): ReactNode {\n  return (\n    <JSONUIProvider\n      registry={registry}\n      initialState={spec.state ?? {}}\n      handlers={actionHandlers}\n      functions={computedFunctions}\n    >\n      <Renderer spec={spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n\nexport default function Page() {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const selected = examples[selectedIndex]!;\n  const [confettiKey, setConfettiKey] = useState(0);\n  const [confettiActive, setConfettiActive] = useState(false);\n\n  const fireConfetti = useCallback(() => {\n    setConfettiKey((k) => k + 1);\n    setConfettiActive(true);\n  }, []);\n\n  useEffect(() => onConfetti(fireConfetti), [fireConfetti]);\n\n  return (\n    <div className=\"h-screen flex flex-col bg-muted/30\">\n      {/* Example selector */}\n      <nav className=\"flex gap-1 p-3 overflow-x-auto border-b bg-background shrink-0\">\n        {examples.map((ex, i) => (\n          <button\n            key={ex.name}\n            onClick={() => setSelectedIndex(i)}\n            className={`px-3 py-1.5 text-sm rounded-md whitespace-nowrap transition-colors ${\n              i === selectedIndex\n                ? \"bg-primary text-primary-foreground\"\n                : \"hover:bg-muted\"\n            }`}\n          >\n            {ex.name}\n          </button>\n        ))}\n      </nav>\n\n      {/* Render area */}\n      <div className=\"flex-1 flex items-start justify-center overflow-auto p-6\">\n        <div className=\"relative bg-background border rounded-lg shadow-sm w-full max-w-[960px]\">\n          {confettiActive && (\n            <div className=\"absolute inset-0 flex items-center justify-center pointer-events-none\">\n              <ConfettiExplosion\n                key={confettiKey}\n                portal={false}\n                force={0.8}\n                duration={3500}\n                particleCount={400}\n                particleSize={8}\n                colors={[\"#00F0FF\", \"#7B61FF\", \"#FF3DFF\", \"#00FF94\", \"#FFE14D\"]}\n                width={1600}\n                height=\"200vh\"\n                zIndex={1}\n                onComplete={() => setConfettiActive(false)}\n              />\n            </div>\n          )}\n          <div className=\"p-6 relative z-10\">\n            <p className=\"text-xs text-muted-foreground mb-4\">\n              {selected.description}\n            </p>\n            <SpecRenderer key={selectedIndex} spec={selected.spec} />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/no-ai/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      \"react/prop-types\": \"off\",\n      \"react/no-unknown-property\": [\"error\", { ignore: [\"jsx\"] }],\n    },\n  },\n];\n"
  },
  {
    "path": "examples/no-ai/lib/examples.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport interface Example {\n  name: string;\n  description: string;\n  spec: Spec;\n}\n\nexport const examples: Example[] = [\n  {\n    name: \"Confetti\",\n    description: \"A button that fires confetti when clicked\",\n    spec: {\n      root: \"btn\",\n      elements: {\n        btn: {\n          type: \"Button\",\n          props: { label: \"Click me\", variant: \"primary\" },\n          on: { press: { action: \"confetti\" } },\n        },\n      },\n    },\n  },\n\n  {\n    name: \"User Profile Card\",\n    description: \"A card with user info, badges, and a progress bar\",\n    spec: {\n      root: \"card\",\n      elements: {\n        card: {\n          type: \"Card\",\n          props: {\n            title: \"User Profile\",\n            description: null,\n            maxWidth: \"md\",\n            centered: null,\n          },\n          children: [\"stack\"],\n        },\n        stack: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"md\",\n            align: null,\n            justify: null,\n          },\n          children: [\"heading\", \"text\", \"badges\", \"sep\", \"progress\"],\n        },\n        heading: {\n          type: \"Heading\",\n          props: { text: \"Jane Cooper\", level: \"h2\" },\n        },\n        text: {\n          type: \"Text\",\n          props: {\n            text: \"Senior software engineer based in San Francisco. Passionate about building accessible, high-performance web applications.\",\n            variant: \"muted\",\n          },\n        },\n        badges: {\n          type: \"Stack\",\n          props: {\n            direction: \"horizontal\",\n            gap: \"sm\",\n            align: null,\n            justify: null,\n          },\n          children: [\"b1\", \"b2\", \"b3\"],\n        },\n        b1: {\n          type: \"Badge\",\n          props: { text: \"TypeScript\", variant: \"default\" },\n        },\n        b2: { type: \"Badge\", props: { text: \"React\", variant: \"secondary\" } },\n        b3: { type: \"Badge\", props: { text: \"Node.js\", variant: \"outline\" } },\n        sep: { type: \"Separator\", props: { orientation: null } },\n        progress: {\n          type: \"Progress\",\n          props: { value: 72, max: 100, label: \"Profile completion\" },\n        },\n      },\n    },\n  },\n\n  {\n    name: \"Settings Form\",\n    description: \"A form with inputs, selects, switches, and checkboxes\",\n    spec: {\n      root: \"card\",\n      state: {\n        name: \"Ada Lovelace\",\n        email: \"ada@example.com\",\n        role: \"Engineer\",\n        notifications: true,\n        darkMode: false,\n      },\n      elements: {\n        card: {\n          type: \"Card\",\n          props: {\n            title: \"Account Settings\",\n            description: \"Manage your preferences\",\n            maxWidth: \"md\",\n            centered: null,\n          },\n          children: [\"form\"],\n        },\n        form: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"md\",\n            align: null,\n            justify: null,\n          },\n          children: [\n            \"nameInput\",\n            \"emailInput\",\n            \"roleSelect\",\n            \"sep\",\n            \"notifSwitch\",\n            \"darkSwitch\",\n            \"sep2\",\n            \"actions\",\n          ],\n        },\n        nameInput: {\n          type: \"Input\",\n          props: {\n            label: \"Full Name\",\n            name: \"name\",\n            type: \"text\",\n            placeholder: \"Your name\",\n            value: { $bindState: \"/name\" },\n            checks: null,\n          },\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            name: \"email\",\n            type: \"email\",\n            placeholder: \"you@example.com\",\n            value: { $bindState: \"/email\" },\n            checks: null,\n          },\n        },\n        roleSelect: {\n          type: \"Select\",\n          props: {\n            label: \"Role\",\n            name: \"role\",\n            options: [\n              \"Engineer\",\n              \"Designer\",\n              \"Product Manager\",\n              \"Data Scientist\",\n            ],\n            placeholder: \"Choose a role\",\n            value: { $bindState: \"/role\" },\n            checks: null,\n          },\n        },\n        sep: { type: \"Separator\", props: { orientation: null } },\n        notifSwitch: {\n          type: \"Switch\",\n          props: {\n            label: \"Email notifications\",\n            name: \"notifications\",\n            checked: { $bindState: \"/notifications\" },\n          },\n        },\n        darkSwitch: {\n          type: \"Switch\",\n          props: {\n            label: \"Dark mode\",\n            name: \"darkMode\",\n            checked: { $bindState: \"/darkMode\" },\n          },\n        },\n        sep2: { type: \"Separator\", props: { orientation: null } },\n        actions: {\n          type: \"Stack\",\n          props: {\n            direction: \"horizontal\",\n            gap: \"sm\",\n            align: null,\n            justify: \"end\",\n          },\n          children: [\"cancelBtn\", \"saveBtn\"],\n        },\n        cancelBtn: {\n          type: \"Button\",\n          props: { label: \"Cancel\", variant: \"secondary\", disabled: null },\n        },\n        saveBtn: {\n          type: \"Button\",\n          props: { label: \"Save Changes\", variant: \"primary\", disabled: null },\n        },\n      },\n    },\n  },\n\n  {\n    name: \"Pricing Table\",\n    description: \"A grid of pricing cards with feature lists\",\n    spec: {\n      root: \"outer\",\n      elements: {\n        outer: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"lg\",\n            align: null,\n            justify: null,\n          },\n          children: [\"header\", \"grid\"],\n        },\n        header: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"sm\",\n            align: \"center\",\n            justify: null,\n          },\n          children: [\"title\", \"subtitle\"],\n        },\n        title: {\n          type: \"Heading\",\n          props: { text: \"Simple, transparent pricing\", level: \"h1\" },\n        },\n        subtitle: {\n          type: \"Text\",\n          props: {\n            text: \"Choose the plan that fits your needs. Upgrade or downgrade at any time.\",\n            variant: \"muted\",\n          },\n        },\n        grid: {\n          type: \"Grid\",\n          props: { columns: 3, gap: \"md\" },\n          children: [\"free\", \"pro\", \"enterprise\"],\n        },\n        free: {\n          type: \"Card\",\n          props: {\n            title: \"Free\",\n            description: \"$0/month\",\n            maxWidth: null,\n            centered: null,\n          },\n          children: [\"freeContent\"],\n        },\n        freeContent: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"sm\",\n            align: null,\n            justify: null,\n          },\n          children: [\"f1\", \"f2\", \"f3\", \"freeBtn\"],\n        },\n        f1: {\n          type: \"Text\",\n          props: { text: \"Up to 3 projects\", variant: \"body\" },\n        },\n        f2: { type: \"Text\", props: { text: \"1 GB storage\", variant: \"body\" } },\n        f3: {\n          type: \"Text\",\n          props: { text: \"Community support\", variant: \"body\" },\n        },\n        freeBtn: {\n          type: \"Button\",\n          props: { label: \"Get Started\", variant: \"secondary\", disabled: null },\n        },\n        pro: {\n          type: \"Card\",\n          props: {\n            title: \"Pro\",\n            description: \"$19/month\",\n            maxWidth: null,\n            centered: null,\n          },\n          children: [\"proContent\"],\n        },\n        proContent: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"sm\",\n            align: null,\n            justify: null,\n          },\n          children: [\"p1\", \"p2\", \"p3\", \"p4\", \"proBtn\"],\n        },\n        p1: {\n          type: \"Text\",\n          props: { text: \"Unlimited projects\", variant: \"body\" },\n        },\n        p2: { type: \"Text\", props: { text: \"50 GB storage\", variant: \"body\" } },\n        p3: {\n          type: \"Text\",\n          props: { text: \"Priority support\", variant: \"body\" },\n        },\n        p4: {\n          type: \"Text\",\n          props: { text: \"Custom domains\", variant: \"body\" },\n        },\n        proBtn: {\n          type: \"Button\",\n          props: {\n            label: \"Upgrade to Pro\",\n            variant: \"primary\",\n            disabled: null,\n          },\n        },\n        enterprise: {\n          type: \"Card\",\n          props: {\n            title: \"Enterprise\",\n            description: \"Custom pricing\",\n            maxWidth: null,\n            centered: null,\n          },\n          children: [\"entContent\"],\n        },\n        entContent: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"sm\",\n            align: null,\n            justify: null,\n          },\n          children: [\"e1\", \"e2\", \"e3\", \"e4\", \"entBtn\"],\n        },\n        e1: {\n          type: \"Text\",\n          props: { text: \"Everything in Pro\", variant: \"body\" },\n        },\n        e2: {\n          type: \"Text\",\n          props: { text: \"Unlimited storage\", variant: \"body\" },\n        },\n        e3: {\n          type: \"Text\",\n          props: { text: \"Dedicated support\", variant: \"body\" },\n        },\n        e4: {\n          type: \"Text\",\n          props: { text: \"SLA guarantees\", variant: \"body\" },\n        },\n        entBtn: {\n          type: \"Button\",\n          props: {\n            label: \"Contact Sales\",\n            variant: \"secondary\",\n            disabled: null,\n          },\n        },\n      },\n    },\n  },\n\n  {\n    name: \"Status Dashboard\",\n    description: \"System status with alerts, a table, and an accordion\",\n    spec: {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"lg\",\n            align: null,\n            justify: null,\n          },\n          children: [\n            \"heading\",\n            \"alertOk\",\n            \"alertWarn\",\n            \"sep\",\n            \"table\",\n            \"sep2\",\n            \"accordion\",\n          ],\n        },\n        heading: {\n          type: \"Heading\",\n          props: { text: \"System Status\", level: \"h1\" },\n        },\n        alertOk: {\n          type: \"Alert\",\n          props: {\n            title: \"API\",\n            message: \"All systems operational -- 99.98% uptime this month\",\n            type: \"success\",\n          },\n        },\n        alertWarn: {\n          type: \"Alert\",\n          props: {\n            title: \"Database\",\n            message: \"Elevated latency detected in us-east-1 region\",\n            type: \"warning\",\n          },\n        },\n        sep: { type: \"Separator\", props: { orientation: null } },\n        table: {\n          type: \"Table\",\n          props: {\n            columns: [\"Service\", \"Status\", \"Latency\", \"Uptime\"],\n            rows: [\n              [\"API Gateway\", \"Operational\", \"12ms\", \"99.99%\"],\n              [\"Auth Service\", \"Operational\", \"8ms\", \"99.98%\"],\n              [\"Database\", \"Degraded\", \"145ms\", \"99.85%\"],\n              [\"CDN\", \"Operational\", \"3ms\", \"100%\"],\n              [\"Workers\", \"Operational\", \"22ms\", \"99.97%\"],\n            ],\n            caption: \"Last updated 2 minutes ago\",\n          },\n        },\n        sep2: { type: \"Separator\", props: { orientation: null } },\n        accordion: {\n          type: \"Accordion\",\n          props: {\n            items: [\n              {\n                title: \"What does 'Degraded' mean?\",\n                content:\n                  \"The service is still operational but experiencing slower response times than normal. We are actively investigating.\",\n              },\n              {\n                title: \"How is uptime calculated?\",\n                content:\n                  \"Uptime is measured over the current calendar month based on successful health-check pings every 30 seconds.\",\n              },\n              {\n                title: \"How do I subscribe to updates?\",\n                content:\n                  \"You can subscribe to incident notifications via email or webhook on the Settings page.\",\n              },\n            ],\n            type: \"single\",\n          },\n        },\n      },\n    },\n  },\n\n  {\n    name: \"Feature Comparison\",\n    description: \"Tables inside an accordion for comparing plan tiers\",\n    spec: {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"lg\",\n            align: null,\n            justify: null,\n          },\n          children: [\"heading\", \"subtitle\", \"accordion\"],\n        },\n        heading: {\n          type: \"Heading\",\n          props: { text: \"Feature Comparison\", level: \"h1\" },\n        },\n        subtitle: {\n          type: \"Text\",\n          props: {\n            text: \"Expand each section to see how plans compare.\",\n            variant: \"muted\",\n          },\n        },\n        accordion: {\n          type: \"Accordion\",\n          props: {\n            items: [\n              {\n                title: \"Projects & Team\",\n                content:\n                  \"Free: 3 projects, 1 member | Pro: Unlimited projects, 10 members | Enterprise: Unlimited everything\",\n              },\n              {\n                title: \"Storage & Bandwidth\",\n                content:\n                  \"Free: 1 GB storage, 10 GB/mo bandwidth | Pro: 50 GB storage, 500 GB/mo | Enterprise: Unlimited\",\n              },\n              {\n                title: \"Support & SLA\",\n                content:\n                  \"Free: Forum, no SLA | Pro: Email + Chat, 99.9% SLA | Enterprise: Dedicated Slack, 99.99% SLA\",\n              },\n            ],\n            type: \"single\",\n          },\n        },\n      },\n    },\n  },\n\n  // =========================================================================\n  // Advanced: Registration form with cross-field validation & $template\n  // =========================================================================\n  {\n    name: \"Registration Form\",\n    description:\n      \"Cross-field validation, $template preview, and validateForm action\",\n    spec: {\n      root: \"card\",\n      state: {\n        form: {\n          name: \"\",\n          email: \"\",\n          password: \"\",\n          confirmPassword: \"\",\n          accountType: \"personal\",\n          company: \"\",\n        },\n        result: null,\n      },\n      elements: {\n        card: {\n          type: \"Card\",\n          props: {\n            title: \"Create Account\",\n            description: \"Fill out the form below to register\",\n            maxWidth: \"md\",\n            centered: null,\n          },\n          children: [\"formStack\"],\n        },\n        formStack: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"md\",\n            align: null,\n            justify: null,\n          },\n          children: [\n            \"preview\",\n            \"sep0\",\n            \"nameInput\",\n            \"emailInput\",\n            \"passwordInput\",\n            \"confirmInput\",\n            \"sep1\",\n            \"accountTypeRadio\",\n            \"companyInput\",\n            \"sep2\",\n            \"actions\",\n            \"statusText\",\n          ],\n        },\n\n        // $template live preview\n        preview: {\n          type: \"Text\",\n          props: {\n            text: {\n              $template: \"Welcome, ${/form/name}! Your email: ${/form/email}\",\n            },\n            variant: \"muted\",\n          },\n          visible: { $state: \"/form/name\", neq: \"\" },\n          children: [],\n        },\n        sep0: { type: \"Separator\", props: { orientation: null }, children: [] },\n\n        nameInput: {\n          type: \"Input\",\n          props: {\n            label: \"Full Name\",\n            name: \"name\",\n            type: \"text\",\n            placeholder: \"Jane Doe\",\n            value: { $bindState: \"/form/name\" },\n            checks: [\n              { type: \"required\", message: \"Name is required\" },\n              {\n                type: \"minLength\",\n                args: { min: 2 },\n                message: \"Name must be at least 2 characters\",\n              },\n            ],\n            validateOn: \"blur\",\n          },\n          children: [],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            name: \"email\",\n            type: \"email\",\n            placeholder: \"jane@example.com\",\n            value: { $bindState: \"/form/email\" },\n            checks: [\n              { type: \"required\", message: \"Email is required\" },\n              { type: \"email\", message: \"Enter a valid email address\" },\n            ],\n            validateOn: \"blur\",\n          },\n          children: [],\n        },\n        passwordInput: {\n          type: \"Input\",\n          props: {\n            label: \"Password\",\n            name: \"password\",\n            type: \"password\",\n            placeholder: \"At least 8 characters\",\n            value: { $bindState: \"/form/password\" },\n            checks: [\n              { type: \"required\", message: \"Password is required\" },\n              {\n                type: \"minLength\",\n                args: { min: 8 },\n                message: \"Password must be at least 8 characters\",\n              },\n            ],\n            validateOn: \"blur\",\n          },\n          children: [],\n        },\n        confirmInput: {\n          type: \"Input\",\n          props: {\n            label: \"Confirm Password\",\n            name: \"confirmPassword\",\n            type: \"password\",\n            placeholder: \"Re-enter your password\",\n            value: { $bindState: \"/form/confirmPassword\" },\n            checks: [\n              { type: \"required\", message: \"Please confirm your password\" },\n              {\n                type: \"matches\",\n                args: { other: { $state: \"/form/password\" } },\n                message: \"Passwords must match\",\n              },\n            ],\n            validateOn: \"blur\",\n          },\n          children: [],\n        },\n        sep1: { type: \"Separator\", props: { orientation: null }, children: [] },\n\n        accountTypeRadio: {\n          type: \"Radio\",\n          props: {\n            label: \"Account Type\",\n            name: \"accountType\",\n            options: [\"personal\", \"business\"],\n            value: { $bindState: \"/form/accountType\" },\n            checks: null,\n            validateOn: null,\n          },\n          children: [],\n        },\n        companyInput: {\n          type: \"Input\",\n          props: {\n            label: \"Company Name\",\n            name: \"company\",\n            type: \"text\",\n            placeholder: \"Acme Inc.\",\n            value: { $bindState: \"/form/company\" },\n            checks: [\n              {\n                type: \"requiredIf\",\n                args: { field: { $state: \"/form/accountType\" } },\n                message: \"Company name is required for business accounts\",\n              },\n            ],\n            validateOn: \"blur\",\n          },\n          visible: { $state: \"/form/accountType\", eq: \"business\" },\n          children: [],\n        },\n        sep2: { type: \"Separator\", props: { orientation: null }, children: [] },\n\n        actions: {\n          type: \"Stack\",\n          props: {\n            direction: \"horizontal\",\n            gap: \"sm\",\n            align: null,\n            justify: \"end\",\n          },\n          children: [\"submitBtn\"],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Register\", variant: \"primary\", disabled: null },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n\n        // Validation result\n        statusText: {\n          type: \"Alert\",\n          props: {\n            title: \"Validation Result\",\n            message: {\n              $cond: { $state: \"/result/valid\", eq: true },\n              $then: \"All fields are valid -- ready to submit!\",\n              $else: \"Please fix the errors above before submitting.\",\n            },\n            type: {\n              $cond: { $state: \"/result/valid\", eq: true },\n              $then: \"success\",\n              $else: \"error\",\n            },\n          },\n          visible: { $state: \"/result\", neq: null },\n          children: [],\n        },\n      },\n    },\n  },\n\n  // =========================================================================\n  // Advanced: Cascading selects with watchers & $computed\n  // =========================================================================\n  {\n    name: \"Cascading Selects\",\n    description:\n      \"Watchers reset dependent fields, $computed derives display values\",\n    spec: {\n      root: \"card\",\n      state: {\n        form: { country: \"\", city: \"\" },\n        availableCities: [],\n      },\n      elements: {\n        card: {\n          type: \"Card\",\n          props: {\n            title: \"Shipping Address\",\n            description: \"Select your country to load available cities\",\n            maxWidth: \"md\",\n            centered: null,\n          },\n          children: [\"formStack\"],\n        },\n        formStack: {\n          type: \"Stack\",\n          props: {\n            direction: \"vertical\",\n            gap: \"md\",\n            align: null,\n            justify: null,\n          },\n          children: [\n            \"countrySelect\",\n            \"citySelect\",\n            \"sep\",\n            \"addressPreview\",\n            \"templatePreview\",\n          ],\n        },\n\n        countrySelect: {\n          type: \"Select\",\n          props: {\n            label: \"Country\",\n            name: \"country\",\n            options: [\"US\", \"Canada\", \"UK\", \"Germany\", \"Japan\"],\n            placeholder: \"Choose a country\",\n            value: { $bindState: \"/form/country\" },\n            checks: [{ type: \"required\", message: \"Country is required\" }],\n            validateOn: \"change\",\n          },\n          watch: {\n            \"/form/country\": [\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/availableCities\",\n                  value: {\n                    $computed: \"citiesForCountry\",\n                    args: { country: { $state: \"/form/country\" } },\n                  },\n                },\n              },\n              {\n                action: \"setState\",\n                params: { statePath: \"/form/city\", value: \"\" },\n              },\n            ],\n          },\n          children: [],\n        },\n\n        citySelect: {\n          type: \"Select\",\n          props: {\n            label: \"City\",\n            name: \"city\",\n            options: { $state: \"/availableCities\" },\n            placeholder: \"Select a city\",\n            value: { $bindState: \"/form/city\" },\n            checks: [{ type: \"required\", message: \"City is required\" }],\n            validateOn: \"change\",\n          },\n          children: [],\n        },\n        sep: { type: \"Separator\", props: { orientation: null }, children: [] },\n\n        // $computed formatted address\n        addressPreview: {\n          type: \"Heading\",\n          props: {\n            text: {\n              $computed: \"formatAddress\",\n              args: {\n                city: { $state: \"/form/city\" },\n                country: { $state: \"/form/country\" },\n              },\n            },\n            level: \"h3\",\n          },\n          children: [],\n        },\n\n        // $template string interpolation\n        templatePreview: {\n          type: \"Text\",\n          props: {\n            text: {\n              $template:\n                \"Shipping to: ${/form/city} in ${/form/country}. Cities available: ${/availableCities}\",\n            },\n            variant: \"muted\",\n          },\n          visible: { $state: \"/form/country\", neq: \"\" },\n          children: [],\n        },\n      },\n    },\n  },\n];\n"
  },
  {
    "path": "examples/no-ai/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    ...shadcnComponentDefinitions,\n  },\n  actions: {\n    confetti: {\n      params: z.object({}),\n      description: \"Fire confetti\",\n    },\n  },\n  functions: {\n    formatAddress: {\n      description:\n        \"Formats country and city into a single address string like 'City, Country'\",\n    },\n    citiesForCountry: {\n      description: \"Returns an array of city names for the given country code\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/no-ai/lib/render/registry.tsx",
    "content": "\"use client\";\n\nimport { defineRegistry } from \"@json-render/react\";\nimport type { ComputedFunction } from \"@json-render/core\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\nimport { catalog } from \"./catalog\";\n\nlet confettiListener: (() => void) | null = null;\n\nexport function onConfetti(cb: () => void) {\n  confettiListener = cb;\n  return () => {\n    confettiListener = null;\n  };\n}\n\nconst cityData: Record<string, string[]> = {\n  US: [\"New York\", \"Los Angeles\", \"Chicago\", \"Houston\", \"Phoenix\"],\n  Canada: [\"Toronto\", \"Vancouver\", \"Montreal\", \"Calgary\", \"Ottawa\"],\n  UK: [\"London\", \"Manchester\", \"Birmingham\", \"Edinburgh\", \"Bristol\"],\n  Germany: [\"Berlin\", \"Munich\", \"Hamburg\", \"Frankfurt\", \"Cologne\"],\n  Japan: [\"Tokyo\", \"Osaka\", \"Kyoto\", \"Yokohama\", \"Sapporo\"],\n};\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    ...shadcnComponents,\n  },\n  actions: {\n    confetti: async () => {\n      confettiListener?.();\n    },\n  },\n});\n\nexport const actionHandlers: Record<\n  string,\n  (params: Record<string, unknown>) => void\n> = {\n  confetti: () => confettiListener?.(),\n};\n\nexport const computedFunctions: Record<string, ComputedFunction> = {\n  formatAddress: (args) => {\n    const city = (args.city as string) ?? \"\";\n    const country = (args.country as string) ?? \"\";\n    if (!city && !country) return \"No location selected\";\n    if (!city) return country;\n    return `${city}, ${country}`;\n  },\n  citiesForCountry: (args) => {\n    const country = (args.country as string) ?? \"\";\n    return cityData[country] ?? [];\n  },\n};\n"
  },
  {
    "path": "examples/no-ai/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/no-ai/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/no-ai/package.json",
    "content": "{\n  \"name\": \"example-no-ai\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless no-ai-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint --max-warnings 0\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/shadcn\": \"workspace:*\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.563.0\",\n    \"next\": \"16.1.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-confetti-explosion\": \"^3.0.3\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"eslint\": \"^9.39.1\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/no-ai/postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "examples/no-ai/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/react-email/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Override the default model used for email generation\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/react-email/CHANGELOG.md",
    "content": "# example-react-email\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react-email@0.14.1\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react-email@0.14.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react-email@0.13.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react-email@0.12.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react-email@0.12.0\n"
  },
  {
    "path": "examples/react-email/app/api/email/route.ts",
    "content": "import {\n  renderToHtml,\n  renderToPlainText,\n} from \"@json-render/react-email/render\";\nimport { examples } from \"@/lib/examples\";\nimport type { Spec } from \"@json-render/core\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const name = searchParams.get(\"name\") ?? \"vercel-invite\";\n  const plain = searchParams.get(\"plain\") === \"1\";\n\n  const example = examples.find((e) => e.name === name);\n  if (!example) {\n    return new Response(\"Example not found\", { status: 404 });\n  }\n\n  return emailResponse(example.spec, plain);\n}\n\nexport async function POST(req: Request) {\n  const { spec, plain } = (await req.json()) as {\n    spec: Spec;\n    plain?: boolean;\n  };\n\n  if (!spec || !spec.root || !spec.elements) {\n    return new Response(\"Invalid spec\", { status: 400 });\n  }\n\n  return emailResponse(spec, plain ?? false);\n}\n\nasync function emailResponse(spec: Spec, plain: boolean) {\n  const content = plain\n    ? await renderToPlainText(spec)\n    : await renderToHtml(spec);\n\n  return new Response(content, {\n    headers: {\n      \"Content-Type\": plain\n        ? \"text/plain; charset=utf-8\"\n        : \"text/html; charset=utf-8\",\n      \"Cache-Control\": \"no-store\",\n    },\n  });\n}\n"
  },
  {
    "path": "examples/react-email/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { buildUserPrompt, type Spec } from \"@json-render/core\";\nimport { emailCatalog } from \"@/lib/catalog\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 60;\n\nconst SYSTEM_PROMPT = emailCatalog.prompt();\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt, startingSpec } = (await req.json()) as {\n    prompt: string;\n    startingSpec?: Spec | null;\n  };\n\n  if (!prompt || typeof prompt !== \"string\") {\n    return Response.json({ error: \"prompt is required\" }, { status: 400 });\n  }\n\n  const userPrompt = buildUserPrompt({\n    prompt,\n    currentSpec: startingSpec,\n  });\n\n  const result = streamText({\n    model: process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    prompt: userPrompt,\n    temperature: 0.7,\n  });\n\n  return result.toTextStreamResponse();\n}\n"
  },
  {
    "path": "examples/react-email/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n    --radius-2xl: calc(var(--radius) + 8px);\n    --radius-3xl: calc(var(--radius) + 12px);\n    --radius-4xl: calc(var(--radius) + 16px);\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n    --radius: 0.625rem;\n    --background: oklch(1 0 0);\n    --foreground: oklch(0.145 0 0);\n    --card: oklch(1 0 0);\n    --card-foreground: oklch(0.145 0 0);\n    --popover: oklch(1 0 0);\n    --popover-foreground: oklch(0.145 0 0);\n    --primary: oklch(0.205 0 0);\n    --primary-foreground: oklch(0.985 0 0);\n    --secondary: oklch(0.97 0 0);\n    --secondary-foreground: oklch(0.205 0 0);\n    --muted: oklch(0.97 0 0);\n    --muted-foreground: oklch(0.556 0 0);\n    --accent: oklch(0.97 0 0);\n    --accent-foreground: oklch(0.205 0 0);\n    --destructive: oklch(0.577 0.245 27.325);\n    --border: oklch(0.922 0 0);\n    --input: oklch(0.922 0 0);\n    --ring: oklch(0.708 0 0);\n    --chart-1: oklch(0.646 0.222 41.116);\n    --chart-2: oklch(0.6 0.118 184.704);\n    --chart-3: oklch(0.398 0.07 227.392);\n    --chart-4: oklch(0.828 0.189 84.429);\n    --chart-5: oklch(0.769 0.188 70.08);\n    --sidebar: oklch(0.985 0 0);\n    --sidebar-foreground: oklch(0.145 0 0);\n    --sidebar-primary: oklch(0.205 0 0);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.97 0 0);\n    --sidebar-accent-foreground: oklch(0.205 0 0);\n    --sidebar-border: oklch(0.922 0 0);\n    --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n    --background: oklch(0.145 0 0);\n    --foreground: oklch(0.985 0 0);\n    --card: oklch(0.205 0 0);\n    --card-foreground: oklch(0.985 0 0);\n    --popover: oklch(0.205 0 0);\n    --popover-foreground: oklch(0.985 0 0);\n    --primary: oklch(0.922 0 0);\n    --primary-foreground: oklch(0.205 0 0);\n    --secondary: oklch(0.269 0 0);\n    --secondary-foreground: oklch(0.985 0 0);\n    --muted: oklch(0.269 0 0);\n    --muted-foreground: oklch(0.708 0 0);\n    --accent: oklch(0.269 0 0);\n    --accent-foreground: oklch(0.985 0 0);\n    --destructive: oklch(0.704 0.191 22.216);\n    --border: oklch(1 0 0 / 10%);\n    --input: oklch(1 0 0 / 15%);\n    --ring: oklch(0.556 0 0);\n    --chart-1: oklch(0.488 0.243 264.376);\n    --chart-2: oklch(0.696 0.17 162.48);\n    --chart-3: oklch(0.769 0.188 70.08);\n    --chart-4: oklch(0.627 0.265 303.9);\n    --chart-5: oklch(0.645 0.246 16.439);\n    --sidebar: oklch(0.205 0 0);\n    --sidebar-foreground: oklch(0.985 0 0);\n    --sidebar-primary: oklch(0.488 0.243 264.376);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.269 0 0);\n    --sidebar-accent-foreground: oklch(0.985 0 0);\n    --sidebar-border: oklch(1 0 0 / 10%);\n    --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "examples/react-email/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"json-render React Email Example\",\n  description:\n    \"Generate HTML emails from JSON specs with @json-render/react-email\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body className={inter.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/react-email/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { examples } from \"@/lib/examples\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport type { Spec } from \"@json-render/core\";\nimport { cn } from \"@/lib/utils\";\n\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/ui/sheet\";\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n} from \"@/components/ui/resizable\";\nimport { FileText, Download, Loader2, ArrowRight, Square } from \"lucide-react\";\n\ntype Mode = \"scratch\" | \"example\";\ntype MobileView = \"json\" | \"preview\";\n\ninterface Selection {\n  mode: Mode;\n  exampleName?: string;\n}\n\nconst HTML_REFRESH_INTERVAL_MS = 2000;\n\nfunction CopyButton({ text }: { text: string }) {\n  const [copied, setCopied] = useState(false);\n\n  return (\n    <button\n      onClick={() => {\n        navigator.clipboard.writeText(text);\n        setCopied(true);\n        setTimeout(() => setCopied(false), 1500);\n      }}\n      className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono\"\n    >\n      {copied ? \"copied\" : \"copy\"}\n    </button>\n  );\n}\n\nfunction isRenderableSpec(spec: Spec | null): spec is Spec {\n  if (!spec?.root || !spec.elements) return false;\n  const root = spec.elements[spec.root];\n  if (!root) return false;\n  if (root.type !== \"Html\" || !root.children?.length) return false;\n  const childTypes = root.children.map((id) => spec.elements[id]?.type);\n  return childTypes.includes(\"Head\") || childTypes.includes(\"Body\");\n}\n\nexport default function Page() {\n  const [selection, setSelection] = useState<Selection>({\n    mode: \"example\",\n    exampleName: examples[0]!.name,\n  });\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n  const [generatedSpec, setGeneratedSpec] = useState<Spec | null>(null);\n  const [htmlContent, setHtmlContent] = useState<string | null>(null);\n  const [error, setError] = useState<string | null>(null);\n  const [mobileView, setMobileView] = useState<MobileView>(\"preview\");\n  const [examplesSheetOpen, setExamplesSheetOpen] = useState(false);\n  const [refreshing, setRefreshing] = useState(false);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const mobileInputRef = useRef<HTMLTextAreaElement>(null);\n  const abortRef = useRef<AbortController | null>(null);\n  const codeScrollRef = useRef<HTMLDivElement>(null);\n  const mobileCodeScrollRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!generating) return;\n    codeScrollRef.current?.scrollTo({\n      top: codeScrollRef.current.scrollHeight,\n    });\n    mobileCodeScrollRef.current?.scrollTo({\n      top: mobileCodeScrollRef.current.scrollHeight,\n    });\n  }, [generating, generatedSpec]);\n\n  const currentExample =\n    selection.mode === \"example\"\n      ? examples.find((e) => e.name === selection.exampleName)\n      : null;\n\n  const activeSpec = generatedSpec ?? currentExample?.spec ?? null;\n\n  useEffect(() => {\n    inputRef.current?.focus();\n  }, [selection.mode, selection.exampleName]);\n\n  const fetchHtml = useCallback(async (spec: Spec, signal?: AbortSignal) => {\n    const res = await fetch(\"/api/email\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ spec }),\n      signal,\n    });\n    if (!res.ok) throw new Error(\"Failed to render email\");\n    const html = await res.text();\n    setHtmlContent(html);\n  }, []);\n\n  // Fetch HTML for example specs on selection change\n  useEffect(() => {\n    if (selection.mode !== \"example\" || !currentExample || generatedSpec)\n      return;\n    fetchHtml(currentExample.spec).catch(() => {});\n  }, [selection.mode, currentExample, generatedSpec, fetchHtml]);\n\n  // Progressive HTML refresh during generation\n  const lastRefreshSpec = useRef<string>(\"\");\n  const generatedSpecRef = useRef<Spec | null>(null);\n  generatedSpecRef.current = generatedSpec;\n\n  useEffect(() => {\n    if (!generating) return;\n\n    const interval = setInterval(() => {\n      const spec = generatedSpecRef.current;\n      if (!spec) return;\n\n      const specKey = JSON.stringify(spec);\n      if (specKey === lastRefreshSpec.current) return;\n      if (!isRenderableSpec(spec)) return;\n\n      lastRefreshSpec.current = specKey;\n      setRefreshing(true);\n      fetchHtml(spec)\n        .catch(() => {})\n        .finally(() => setRefreshing(false));\n    }, HTML_REFRESH_INTERVAL_MS);\n\n    return () => clearInterval(interval);\n  }, [generating, fetchHtml]);\n\n  const handleGenerate = useCallback(async () => {\n    if (!prompt.trim()) return;\n\n    abortRef.current?.abort();\n    const controller = new AbortController();\n    abortRef.current = controller;\n\n    setGenerating(true);\n    setError(null);\n    lastRefreshSpec.current = \"\";\n\n    try {\n      const startingSpec =\n        selection.mode === \"example\" && currentExample\n          ? currentExample.spec\n          : null;\n\n      const res = await fetch(\"/api/generate\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt: prompt.trim(), startingSpec }),\n        signal: controller.signal,\n      });\n      if (!res.ok) throw new Error(\"Generation failed\");\n\n      const reader = res.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n      const compiler = createSpecStreamCompiler<Spec>(\n        startingSpec ? { ...startingSpec } : {},\n      );\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        const chunk = decoder.decode(value, { stream: true });\n        const { result, newPatches } = compiler.push(chunk);\n        if (newPatches.length > 0) setGeneratedSpec(result);\n      }\n\n      const finalSpec = compiler.getResult();\n      setGeneratedSpec(finalSpec);\n      setGenerating(false);\n\n      await fetchHtml(finalSpec);\n    } catch (e) {\n      if (controller.signal.aborted) return;\n      setError(e instanceof Error ? e.message : \"Something went wrong\");\n      setGenerating(false);\n    }\n  }, [prompt, selection, currentExample, fetchHtml]);\n\n  const handleStop = useCallback(() => {\n    abortRef.current?.abort();\n    setGenerating(false);\n\n    if (isRenderableSpec(generatedSpec)) {\n      fetchHtml(generatedSpec).catch(() => {});\n    }\n  }, [generatedSpec, fetchHtml]);\n\n  const select = (next: Selection) => {\n    abortRef.current?.abort();\n    setSelection(next);\n    setGeneratedSpec(null);\n    setHtmlContent(null);\n    setError(null);\n    setPrompt(\"\");\n    setGenerating(false);\n    setExamplesSheetOpen(false);\n  };\n\n  const handleDownload = async () => {\n    if (!activeSpec) return;\n    const spec = generatedSpec ?? currentExample?.spec;\n    if (!spec) return;\n\n    const res = await fetch(\"/api/email\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ spec }),\n    });\n    if (!res.ok) return;\n    const html = await res.text();\n    const blob = new Blob([html], { type: \"text/html\" });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = \"email.html\";\n    a.click();\n    URL.revokeObjectURL(url);\n  };\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleGenerate();\n      }\n    },\n    [handleGenerate],\n  );\n\n  const jsonCode = activeSpec\n    ? JSON.stringify(activeSpec, null, 2)\n    : \"// select an example or generate an email\";\n\n  // ---------------------------------------------------------------------------\n  // Pane: Chat / Examples\n  // ---------------------------------------------------------------------------\n  const chatPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-2\">\n        <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />\n        <span className=\"text-xs font-mono text-muted-foreground\">\n          json-render / react-email\n        </span>\n      </div>\n\n      <ScrollArea className=\"flex-1\">\n        <div className=\"p-2 space-y-1\">\n          <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n            start\n          </p>\n          <button\n            onClick={() => select({ mode: \"scratch\" })}\n            className={cn(\n              \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n              selection.mode === \"scratch\"\n                ? \"bg-muted text-foreground\"\n                : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n            )}\n          >\n            <span className=\"font-medium\">From scratch</span>\n          </button>\n\n          <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n            examples\n          </p>\n          {examples.map((ex) => (\n            <button\n              key={ex.name}\n              onClick={() => select({ mode: \"example\", exampleName: ex.name })}\n              className={cn(\n                \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                selection.mode === \"example\" &&\n                  selection.exampleName === ex.name\n                  ? \"bg-muted text-foreground\"\n                  : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n              )}\n            >\n              <span className=\"font-medium\">{ex.label}</span>\n              <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                {ex.description}\n              </p>\n            </button>\n          ))}\n        </div>\n      </ScrollArea>\n\n      <div\n        className=\"border-t border-border p-3 cursor-text\"\n        onMouseDown={(e) => {\n          const target = e.target as HTMLElement;\n          if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n            e.preventDefault();\n            inputRef.current?.focus();\n          }\n        }}\n      >\n        {error && (\n          <div className=\"mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive\">\n            {error}\n          </div>\n        )}\n        <textarea\n          ref={inputRef}\n          value={prompt}\n          onChange={(e) => setPrompt(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={\n            selection.mode === \"scratch\"\n              ? \"Describe the email you want...\"\n              : `Modify the ${currentExample?.label ?? \"example\"}...`\n          }\n          className=\"w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50\"\n          rows={2}\n          autoFocus\n        />\n        <div className=\"flex justify-between items-center mt-2\">\n          <span className=\"text-[11px] text-muted-foreground\">\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </span>\n          {generating ? (\n            <button\n              onClick={handleStop}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n              aria-label=\"Stop\"\n            >\n              <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n            </button>\n          ) : (\n            <button\n              onClick={handleGenerate}\n              disabled={!prompt.trim()}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n              aria-label=\"Generate\"\n            >\n              <ArrowRight className=\"h-3.5 w-3.5\" />\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: JSON Spec\n  // ---------------------------------------------------------------------------\n  const codePane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">json</span>\n        {generating && (\n          <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && <CopyButton text={jsonCode} />}\n      </div>\n      <div ref={codeScrollRef} className=\"flex-1 overflow-auto\">\n        <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n          {jsonCode}\n        </pre>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: Email Preview\n  // ---------------------------------------------------------------------------\n  const previewPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">preview</span>\n        {(generating || refreshing) && (\n          <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && (\n          <button\n            onClick={handleDownload}\n            className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1\"\n          >\n            <Download className=\"h-3 w-3\" />\n            download\n          </button>\n        )}\n      </div>\n      <div className=\"flex-1 relative bg-neutral-600\">\n        {htmlContent ? (\n          <div className=\"h-full flex justify-center p-5 overflow-auto\">\n            <iframe\n              srcDoc={htmlContent}\n              className=\"w-[620px] h-full border-none bg-white shadow-sm\"\n              title=\"Email preview\"\n            />\n          </div>\n        ) : (\n          <div className=\"h-full flex flex-col items-center justify-center gap-2 text-neutral-400\">\n            <FileText className=\"h-10 w-10\" />\n            <p className=\"text-sm\">\n              {selection.mode === \"scratch\"\n                ? \"Enter a prompt to generate an email\"\n                : \"Select an example to preview\"}\n            </p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Render\n  // ---------------------------------------------------------------------------\n  return (\n    <div className=\"h-dvh flex flex-col\">\n      {/* Desktop: 3-pane resizable layout */}\n      <div className=\"hidden lg:flex flex-1 min-h-0\">\n        <ResizablePanelGroup className=\"flex-1\">\n          <ResizablePanel defaultSize={25} minSize={15}>\n            {chatPane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={35} minSize={20}>\n            {codePane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={40} minSize={20}>\n            {previewPane}\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      </div>\n\n      {/* Mobile: toolbar + content + prompt */}\n      <div className=\"flex lg:hidden flex-col flex-1 min-h-0\">\n        <div className=\"border-b border-border px-3 h-9 flex items-center gap-3 shrink-0\">\n          <button\n            onClick={() => setExamplesSheetOpen(true)}\n            className=\"text-xs font-mono font-medium px-1.5 py-0.5 rounded bg-muted text-foreground shrink-0\"\n          >\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </button>\n          {([\"json\", \"preview\"] as const).map((tab) => (\n            <button\n              key={tab}\n              onClick={() => setMobileView(tab)}\n              className={cn(\n                \"text-xs font-mono transition-colors shrink-0\",\n                mobileView === tab\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground hover:text-foreground\",\n              )}\n            >\n              {tab}\n            </button>\n          ))}\n          {(generating || refreshing) && (\n            <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin shrink-0\" />\n          )}\n          <div className=\"flex-1\" />\n          {activeSpec && (\n            <button\n              onClick={handleDownload}\n              className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1\"\n            >\n              <Download className=\"h-3 w-3\" />\n            </button>\n          )}\n        </div>\n\n        <div ref={mobileCodeScrollRef} className=\"flex-1 min-h-0 overflow-auto\">\n          {mobileView === \"json\" ? (\n            <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n              {jsonCode}\n            </pre>\n          ) : (\n            <div className=\"h-full relative bg-neutral-600\">\n              {htmlContent ? (\n                <div className=\"h-full flex justify-center p-5 overflow-auto\">\n                  <iframe\n                    srcDoc={htmlContent}\n                    className=\"w-[620px] h-full border-none bg-white shadow-sm\"\n                    title=\"Email preview\"\n                  />\n                </div>\n              ) : (\n                <div className=\"h-full flex flex-col items-center justify-center gap-2 text-neutral-400\">\n                  <FileText className=\"h-10 w-10\" />\n                  <p className=\"text-sm\">Enter a prompt to generate an email</p>\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n\n        <div\n          className=\"border-t border-border p-3 shrink-0 cursor-text\"\n          onMouseDown={(e) => {\n            const target = e.target as HTMLElement;\n            if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n              e.preventDefault();\n              mobileInputRef.current?.focus();\n            }\n          }}\n        >\n          <textarea\n            ref={mobileInputRef}\n            value={prompt}\n            onChange={(e) => setPrompt(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder={\n              selection.mode === \"scratch\"\n                ? \"Describe the email you want...\"\n                : `Modify the ${currentExample?.label ?? \"example\"}...`\n            }\n            className=\"w-full bg-background text-base resize-none outline-none placeholder:text-muted-foreground/50\"\n            rows={2}\n          />\n          <div className=\"flex justify-between items-center mt-2\">\n            <span className=\"text-[11px] text-muted-foreground\">\n              {selection.mode === \"example\" && currentExample\n                ? currentExample.label\n                : \"scratch\"}\n            </span>\n            {generating ? (\n              <button\n                onClick={handleStop}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n                aria-label=\"Stop\"\n              >\n                <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n              </button>\n            ) : (\n              <button\n                onClick={handleGenerate}\n                disabled={!prompt.trim()}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n                aria-label=\"Generate\"\n              >\n                <ArrowRight className=\"h-3.5 w-3.5\" />\n              </button>\n            )}\n          </div>\n        </div>\n\n        <Sheet open={examplesSheetOpen} onOpenChange={setExamplesSheetOpen}>\n          <SheetContent side=\"left\" className=\"w-80 p-0\">\n            <SheetTitle className=\"sr-only\">Examples</SheetTitle>\n            <ScrollArea className=\"h-full\">\n              <div className=\"p-2 space-y-1\">\n                <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n                  start\n                </p>\n                <button\n                  onClick={() => select({ mode: \"scratch\" })}\n                  className={cn(\n                    \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                    selection.mode === \"scratch\"\n                      ? \"bg-muted text-foreground\"\n                      : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                  )}\n                >\n                  From scratch\n                </button>\n                <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n                  examples\n                </p>\n                {examples.map((ex) => (\n                  <button\n                    key={ex.name}\n                    onClick={() =>\n                      select({ mode: \"example\", exampleName: ex.name })\n                    }\n                    className={cn(\n                      \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                      selection.mode === \"example\" &&\n                        selection.exampleName === ex.name\n                        ? \"bg-muted text-foreground\"\n                        : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                    )}\n                  >\n                    <span className=\"font-medium\">{ex.label}</span>\n                    <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                      {ex.description}\n                    </p>\n                  </button>\n                ))}\n              </div>\n            </ScrollArea>\n          </SheetContent>\n        </Sheet>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/react-email/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "examples/react-email/components/ui/resizable.tsx",
    "content": "\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ResizablePanelGroup({\n  className,\n  ...props\n}: ResizablePrimitive.GroupProps) {\n  return (\n    <ResizablePrimitive.Group\n      data-slot=\"resizable-panel-group\"\n      className={cn(\n        \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {\n  return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({\n  withHandle,\n  className,\n  ...props\n}: ResizablePrimitive.SeparatorProps & {\n  withHandle?: boolean;\n}) {\n  return (\n    <ResizablePrimitive.Separator\n      data-slot=\"resizable-handle\"\n      className={cn(\n        \"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      {withHandle && (\n        <div className=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n          <GripVerticalIcon className=\"size-2.5\" />\n        </div>\n      )}\n    </ResizablePrimitive.Separator>\n  );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"
  },
  {
    "path": "examples/react-email/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ScrollArea({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"relative\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        data-slot=\"scroll-area-viewport\"\n        className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  );\n}\n\nfunction ScrollBar({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      className={cn(\n        \"flex touch-none p-px transition-colors select-none\",\n        orientation === \"vertical\" &&\n          \"h-full w-2.5 border-l border-l-transparent\",\n        orientation === \"horizontal\" &&\n          \"h-2.5 flex-col border-t border-t-transparent\",\n        className,\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-border relative flex-1 rounded-full\"\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  );\n}\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "examples/react-email/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "examples/react-email/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  showCloseButton?: boolean;\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n            <XIcon className=\"size-4\" />\n            <span className=\"sr-only\">Close</span>\n          </SheetPrimitive.Close>\n        )}\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "examples/react-email/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst tabsListVariants = cva(\n  \"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction TabsList({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n  VariantProps<typeof tabsListVariants>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground\",\n        \"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n"
  },
  {
    "path": "examples/react-email/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "examples/react-email/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rtl\": false,\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "examples/react-email/lib/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-email/server\";\nimport { standardComponentDefinitions } from \"@json-render/react-email/catalog\";\n\nexport const emailCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n  actions: {},\n});\n"
  },
  {
    "path": "examples/react-email/lib/examples.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport interface Example {\n  name: string;\n  label: string;\n  description: string;\n  spec: Spec;\n}\n\nconst staticUrl = \"/static\";\n\nexport const examples: Example[] = [\n  {\n    name: \"vercel-invite\",\n    label: \"Vercel Invite\",\n    description: \"Team invitation email with avatars and CTA\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: { text: \"Join Alan on Vercel\" },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              fontFamily:\n                '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Ubuntu, sans-serif',\n              margin: \"0 auto\",\n              padding: \"0 8px\",\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              maxWidth: \"465px\",\n              margin: \"40px auto\",\n              border: \"1px solid #eaeaea\",\n              borderRadius: \"4px\",\n              padding: \"20px\",\n            },\n          },\n          children: [\n            \"logo-section\",\n            \"heading\",\n            \"greeting-text\",\n            \"invite-text\",\n            \"avatar-section\",\n            \"button-section\",\n            \"url-text\",\n            \"invite-link\",\n            \"hr\",\n            \"footer-text\",\n          ],\n        },\n        \"logo-section\": {\n          type: \"Section\",\n          props: { style: { marginTop: \"32px\" } },\n          children: [\"logo\"],\n        },\n        logo: {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-logo.png`,\n            width: 40,\n            height: 37,\n            alt: \"Vercel\",\n            style: { margin: \"0 auto\", display: \"block\" },\n          },\n          children: [],\n        },\n        heading: {\n          type: \"Heading\",\n          props: {\n            text: \"Join Enigma on Vercel\",\n            as: \"h1\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"24px\",\n              fontWeight: \"normal\",\n              textAlign: \"center\",\n              margin: \"30px 0\",\n              padding: \"0\",\n            },\n          },\n          children: [],\n        },\n        \"greeting-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Hello alanturing,\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"invite-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Alan (alan.turing@example.com) has invited you to the Enigma team on Vercel.\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"avatar-section\": {\n          type: \"Section\",\n          props: { style: {} },\n          children: [\"avatar-row\"],\n        },\n        \"avatar-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"user-avatar-col\", \"arrow-col\", \"team-avatar-col\"],\n        },\n        \"user-avatar-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"user-avatar\"],\n        },\n        \"user-avatar\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-user.png`,\n            width: 64,\n            height: 64,\n            alt: \"alanturing\",\n            style: { borderRadius: \"50%\", marginLeft: \"auto\" },\n          },\n          children: [],\n        },\n        \"arrow-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"arrow-img\"],\n        },\n        \"arrow-img\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-arrow.png`,\n            width: 12,\n            height: 9,\n            alt: \"invited to\",\n            style: { margin: \"0 auto\" },\n          },\n          children: [],\n        },\n        \"team-avatar-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"team-avatar\"],\n        },\n        \"team-avatar\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-team.png`,\n            width: 64,\n            height: 64,\n            alt: \"Enigma\",\n            style: { borderRadius: \"50%\", marginRight: \"auto\" },\n          },\n          children: [],\n        },\n        \"button-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              marginTop: \"32px\",\n              marginBottom: \"32px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [\"join-button\"],\n        },\n        \"join-button\": {\n          type: \"Button\",\n          props: {\n            text: \"Join the team\",\n            href: \"https://vercel.com/teams/invite/foo\",\n            style: {\n              backgroundColor: \"#000000\",\n              borderRadius: \"4px\",\n              color: \"#ffffff\",\n              fontSize: \"12px\",\n              fontWeight: \"600\",\n              textDecoration: \"none\",\n              textAlign: \"center\",\n              padding: \"12px 20px\",\n            },\n          },\n          children: [],\n        },\n        \"url-text\": {\n          type: \"Text\",\n          props: {\n            text: \"or copy and paste this URL into your browser:\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"invite-link\": {\n          type: \"Link\",\n          props: {\n            text: \"https://vercel.com/teams/invite/foo\",\n            href: \"https://vercel.com/teams/invite/foo\",\n            style: {\n              color: \"#2563eb\",\n              textDecoration: \"none\",\n              fontSize: \"14px\",\n            },\n          },\n          children: [],\n        },\n        hr: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#eaeaea\", margin: \"26px 0\" } },\n          children: [],\n        },\n        \"footer-text\": {\n          type: \"Text\",\n          props: {\n            text: \"This invitation was intended for alanturing. This invite was sent from 204.13.186.218 located in São Paulo, Brazil. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.\",\n            style: {\n              color: \"#666666\",\n              fontSize: \"12px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n  {\n    name: \"stripe-welcome\",\n    label: \"Stripe Welcome\",\n    description: \"Onboarding email with dashboard CTA\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: {\n            text: \"You're now ready to make live transactions with Stripe!\",\n          },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#f6f9fc\",\n              fontFamily:\n                '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Ubuntu, sans-serif',\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              margin: \"0 auto\",\n              padding: \"20px 0 48px\",\n              marginBottom: \"64px\",\n            },\n          },\n          children: [\"content-section\"],\n        },\n        \"content-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"0 48px\" } },\n          children: [\n            \"logo\",\n            \"hr1\",\n            \"text-intro\",\n            \"text-dashboard\",\n            \"cta-button\",\n            \"hr2\",\n            \"text-docs\",\n            \"docs-link\",\n            \"text-api-keys\",\n            \"text-checklist\",\n            \"text-support\",\n            \"text-signoff\",\n            \"hr3\",\n            \"footer-text\",\n          ],\n        },\n        logo: {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/stripe-logo.png`,\n            width: 49,\n            height: 21,\n            alt: \"Stripe\",\n            style: null,\n          },\n          children: [],\n        },\n        hr1: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"text-intro\": {\n          type: \"Text\",\n          props: {\n            text: \"Thanks for submitting your account information. You're now ready to make live transactions with Stripe!\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-dashboard\": {\n          type: \"Text\",\n          props: {\n            text: \"You can view your payments and a variety of other information about your account right from your dashboard.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"cta-button\": {\n          type: \"Button\",\n          props: {\n            text: \"View your Stripe Dashboard\",\n            href: \"https://dashboard.stripe.com/login\",\n            style: {\n              backgroundColor: \"#656ee8\",\n              borderRadius: \"3px\",\n              color: \"#ffffff\",\n              fontSize: \"16px\",\n              fontWeight: \"bold\",\n              textDecoration: \"none\",\n              textAlign: \"center\",\n              display: \"block\",\n              padding: \"10px\",\n            },\n          },\n          children: [],\n        },\n        hr2: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"text-docs\": {\n          type: \"Text\",\n          props: {\n            text: \"If you haven't finished your integration, you might find our docs handy.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"docs-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Stripe Documentation — Getting Started\",\n            href: \"https://docs.stripe.com/dashboard/basics\",\n            style: {\n              color: \"#556cd6\",\n              fontSize: \"16px\",\n            },\n          },\n          children: [],\n        },\n        \"text-api-keys\": {\n          type: \"Text\",\n          props: {\n            text: \"Once you're ready to start accepting payments, you'll just need to use your live API keys instead of your test API keys. Your account can simultaneously be used for both test and live requests, so you can continue testing while accepting live payments. Check out our tutorial about account basics.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-checklist\": {\n          type: \"Text\",\n          props: {\n            text: \"Finally, we've put together a quick checklist to ensure your website conforms to card network standards.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-support\": {\n          type: \"Text\",\n          props: {\n            text: \"We'll be here to help you with any step along the way. You can find answers to most questions and get in touch with us on our support site.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-signoff\": {\n          type: \"Text\",\n          props: {\n            text: \"— The Stripe team\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        hr3: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"footer-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080\",\n            style: {\n              color: \"#8898aa\",\n              fontSize: \"12px\",\n              lineHeight: \"16px\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n  {\n    name: \"nike-receipt\",\n    label: \"Nike Receipt\",\n    description: \"Order shipment notification with product details\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: {\n            text: \"Get your order summary, estimated delivery date and more\",\n          },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              fontFamily: '\"Helvetica Neue\", Helvetica, Arial, sans-serif',\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              margin: \"10px auto\",\n              width: \"600px\",\n              maxWidth: \"100%\",\n              border: \"1px solid #E5E5E5\",\n            },\n          },\n          children: [\n            \"tracking-section\",\n            \"hr1\",\n            \"hero-section\",\n            \"hr2\",\n            \"shipping-section\",\n            \"hr3\",\n            \"product-section\",\n            \"hr4\",\n            \"order-info-section\",\n            \"hr5\",\n            \"footer-section\",\n          ],\n        },\n\n        // ── Tracking Section ──\n        \"tracking-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              padding: \"22px 40px\",\n              backgroundColor: \"#F7F7F7\",\n            },\n          },\n          children: [\"tracking-row\"],\n        },\n        \"tracking-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"tracking-info-col\", \"tracking-btn-col\"],\n        },\n        \"tracking-info-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"tracking-label\", \"tracking-number\"],\n        },\n        \"tracking-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Tracking Number\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"tracking-number\": {\n          type: \"Text\",\n          props: {\n            text: \"1ZV218970300071628\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"tracking-btn-col\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"right\" } },\n          children: [\"tracking-link\"],\n        },\n        \"tracking-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Track Package\",\n            href: \"https://www.nike.com/orders\",\n            style: {\n              border: \"1px solid #929292\",\n              fontSize: \"16px\",\n              textDecoration: \"none\",\n              padding: \"10px 0\",\n              width: \"220px\",\n              display: \"block\",\n              textAlign: \"center\",\n              fontWeight: \"500\",\n              color: \"#000000\",\n            },\n          },\n          children: [],\n        },\n        hr1: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Hero Section ──\n        \"hero-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              padding: \"40px 74px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [\n            \"nike-logo\",\n            \"hero-heading\",\n            \"hero-text\",\n            \"hero-text-payment\",\n          ],\n        },\n        \"nike-logo\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/nike-logo.png`,\n            width: 66,\n            height: 22,\n            alt: \"Nike\",\n            style: { margin: \"0 auto\", display: \"block\" },\n          },\n          children: [],\n        },\n        \"hero-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"It's On Its Way.\",\n            as: \"h1\",\n            style: {\n              fontSize: \"32px\",\n              lineHeight: \"1.3\",\n              fontWeight: \"bold\",\n              textAlign: \"center\",\n              letterSpacing: \"-1px\",\n            },\n          },\n          children: [],\n        },\n        \"hero-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Your order is on its way. Use the link above to track its progress.\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        \"hero-text-payment\": {\n          type: \"Text\",\n          props: {\n            text: \"We've also charged your payment method for the cost of your order and will be removing any authorization holds. For payment details, please visit your Orders page on Nike.com or in the Nike app.\",\n            style: {\n              margin: \"24px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr2: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Shipping Section ──\n        \"shipping-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 40px\" } },\n          children: [\"shipping-label\", \"shipping-address\"],\n        },\n        \"shipping-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Shipping to: Alan Turing\",\n            style: {\n              margin: \"0\",\n              fontSize: \"15px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"shipping-address\": {\n          type: \"Text\",\n          props: {\n            text: \"2125 Chestnut St, San Francisco, CA 94123\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr3: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Product Section ──\n        \"product-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"40px\" } },\n          children: [\"product-row\"],\n        },\n        \"product-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"product-img-col\", \"product-details-col\"],\n        },\n        \"product-img-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"product-img\"],\n        },\n        \"product-img\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/nike-product.png`,\n            alt: \"Brazil 2022/23 Stadium Away Women's Nike Dri-FIT Soccer Jersey\",\n            width: 260,\n            height: null,\n            style: { float: \"left\" },\n          },\n          children: [],\n        },\n        \"product-details-col\": {\n          type: \"Column\",\n          props: {\n            style: { verticalAlign: \"top\", paddingLeft: \"12px\" },\n          },\n          children: [\"product-name\", \"product-size\"],\n        },\n        \"product-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Brazil 2022/23 Stadium Away Women's Nike Dri-FIT Soccer Jersey\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        \"product-size\": {\n          type: \"Text\",\n          props: {\n            text: \"Size L (12–14)\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr4: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Order Info Section ──\n        \"order-info-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 40px\" } },\n          children: [\"order-meta-row\", \"order-status-row\"],\n        },\n        \"order-meta-row\": {\n          type: \"Row\",\n          props: { style: { marginBottom: \"40px\" } },\n          children: [\"order-number-col\", \"order-date-col\"],\n        },\n        \"order-number-col\": {\n          type: \"Column\",\n          props: { style: { width: \"170px\" } },\n          children: [\"order-number-label\", \"order-number-value\"],\n        },\n        \"order-number-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Order Number\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"order-number-value\": {\n          type: \"Text\",\n          props: {\n            text: \"C0106373851\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"order-date-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"order-date-label\", \"order-date-value\"],\n        },\n        \"order-date-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Order Date\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"order-date-value\": {\n          type: \"Text\",\n          props: {\n            text: \"Sep 22, 2022\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"order-status-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"order-status-col\"],\n        },\n        \"order-status-col\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"order-status-link\"],\n        },\n        \"order-status-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Order Status\",\n            href: \"https://www.nike.com/orders\",\n            style: {\n              border: \"1px solid #929292\",\n              fontSize: \"16px\",\n              textDecoration: \"none\",\n              padding: \"10px 0\",\n              width: \"220px\",\n              display: \"block\",\n              textAlign: \"center\",\n              fontWeight: \"500\",\n              color: \"#000000\",\n              margin: \"0 auto\",\n            },\n          },\n          children: [],\n        },\n        hr5: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Footer Section ──\n        \"footer-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 0\", textAlign: \"center\" } },\n          children: [\n            \"footer-brand\",\n            \"footer-nav-row\",\n            \"footer-contact\",\n            \"footer-copyright\",\n            \"footer-address\",\n          ],\n        },\n        \"footer-brand\": {\n          type: \"Text\",\n          props: {\n            text: \"Nike.com\",\n            style: {\n              fontSize: \"32px\",\n              lineHeight: \"1.3\",\n              fontWeight: \"bold\",\n              textAlign: \"center\",\n              letterSpacing: \"-1px\",\n            },\n          },\n          children: [],\n        },\n        \"footer-nav-row\": {\n          type: \"Row\",\n          props: { style: { width: \"370px\", margin: \"0 auto\" } },\n          children: [\n            \"footer-nav-men\",\n            \"footer-nav-women\",\n            \"footer-nav-kids\",\n            \"footer-nav-customize\",\n          ],\n        },\n        \"footer-nav-men\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-men\"],\n        },\n        \"link-men\": {\n          type: \"Link\",\n          props: {\n            text: \"Men\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-women\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-women\"],\n        },\n        \"link-women\": {\n          type: \"Link\",\n          props: {\n            text: \"Women\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-kids\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-kids\"],\n        },\n        \"link-kids\": {\n          type: \"Link\",\n          props: {\n            text: \"Kids\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-customize\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-customize\"],\n        },\n        \"link-customize\": {\n          type: \"Link\",\n          props: {\n            text: \"Customize\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-contact\": {\n          type: \"Text\",\n          props: {\n            text: \"Please contact us if you have any questions. (If you reply to this email, we won't be able to see it.)\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n              padding: \"30px 0\",\n            },\n          },\n          children: [],\n        },\n        \"footer-copyright\": {\n          type: \"Text\",\n          props: {\n            text: \"© 2022 Nike, Inc. All Rights Reserved.\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [],\n        },\n        \"footer-address\": {\n          type: \"Text\",\n          props: {\n            text: \"NIKE, INC. One Bowerman Drive, Beaverton, Oregon 97005, USA.\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n];\n"
  },
  {
    "path": "examples/react-email/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:react-email:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:react-email:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/react-email/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/react-email/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/react-email/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  serverExternalPackages: [\n    \"@react-email/components\",\n    \"@react-email/render\",\n    \"@json-render/react-email\",\n  ],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/react-email/package.json",
    "content": "{\n  \"name\": \"example-react-email\",\n  \"version\": \"0.1.5\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"portless react-email-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.52\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react-email\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"ai\": \"6.0.94\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.575.0\",\n    \"next\": \"16.1.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-resizable-panels\": \"^4.4.1\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"shadcn\": \"^3.8.5\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/react-email/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/react-email/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/react-native/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Override the default model used for UI generation\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n"
  },
  {
    "path": "examples/react-native/CHANGELOG.md",
    "content": "# example-react-native\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react-native@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react-native@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react-native@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react-native@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react-native@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react-native@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react-native@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react-native@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react-native@0.9.0\n"
  },
  {
    "path": "examples/react-native/app/_layout.tsx",
    "content": "import { Stack } from \"expo-router\";\nimport { StatusBar } from \"expo-status-bar\";\n\nexport default function RootLayout() {\n  return (\n    <>\n      <StatusBar style=\"dark\" />\n      <Stack\n        screenOptions={{\n          headerShown: false,\n          contentStyle: { backgroundColor: \"#ffffff\" },\n        }}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "examples/react-native/app/api/generate+api.ts",
    "content": "import { streamText } from \"ai\";\nimport { createGatewayProvider } from \"@ai-sdk/gateway\";\nimport { buildUserPrompt } from \"@json-render/core\";\nimport { catalog, customRules } from \"../../lib/render/catalog\";\n\nconst SYSTEM_PROMPT = catalog.prompt({ customRules });\n\nconst MAX_PROMPT_LENGTH = 500;\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\n// Explicitly pass the API key since Expo's Metro bundler only inlines\n// process.env values that are directly referenced in application code.\n// The default `gateway` singleton reads process.env.AI_GATEWAY_API_KEY\n// internally, which Metro won't bundle.\nconst gateway = createGatewayProvider({\n  apiKey: process.env.AI_GATEWAY_API_KEY,\n});\n\nexport async function POST(req: Request) {\n  console.log(\"[API] POST /api/generate called\");\n  console.log(\"[API] API key present:\", !!process.env.AI_GATEWAY_API_KEY);\n  console.log(\"[API] Model:\", process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL);\n\n  try {\n    const { prompt, context } = await req.json();\n    console.log(\"[API] prompt:\", prompt);\n\n    const userPrompt = buildUserPrompt({\n      prompt,\n      currentSpec: context?.previousSpec,\n      state: context?.state,\n      maxPromptLength: MAX_PROMPT_LENGTH,\n    });\n\n    const modelId = process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL;\n    const model = gateway(modelId);\n\n    console.log(\"[API] calling streamText with model:\", modelId);\n    const result = streamText({\n      model,\n      system: SYSTEM_PROMPT,\n      prompt: userPrompt,\n      temperature: 0.7,\n    });\n\n    console.log(\"[API] returning text stream response\");\n    return result.toTextStreamResponse();\n  } catch (error) {\n    const message = error instanceof Error ? error.message : \"Unknown error\";\n    console.error(\"API generate error:\", message);\n    return new Response(JSON.stringify({ error: message }), {\n      status: 500,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }\n}\n"
  },
  {
    "path": "examples/react-native/app/index.tsx",
    "content": "import React, { useState, useRef, useCallback, useEffect } from \"react\";\nimport {\n  View,\n  Text,\n  TextInput,\n  Pressable,\n  ScrollView,\n  Animated,\n  Easing,\n  Platform,\n  Keyboard,\n  StyleSheet,\n} from \"react-native\";\nimport { fetch } from \"expo/fetch\";\nimport Constants from \"expo-constants\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\nimport * as Clipboard from \"expo-clipboard\";\nimport { useUIStream } from \"@json-render/react-native\";\nimport { AppRenderer } from \"../lib/render/renderer\";\n\n// Resolve the Metro dev server origin for API route calls.\n// expo/fetch doesn't resolve relative URLs like the built-in RN fetch does.\nfunction getApiBaseUrl(): string {\n  const debuggerHost =\n    Constants.expoConfig?.hostUri ??\n    Constants.manifest2?.extra?.expoGo?.debuggerHost;\n  if (debuggerHost) {\n    const host = debuggerHost.split(\":\").shift();\n    // Metro dev server runs on port 8081 by default\n    return `http://${host}:8081`;\n  }\n  return \"http://localhost:8081\";\n}\n\nconst API_BASE = getApiBaseUrl();\n\nexport default function HomeScreen() {\n  const insets = useSafeAreaInsets();\n  const [prompt, setPrompt] = useState(\"\");\n\n  const { spec, isStreaming, error, rawLines, send, stop, clear } = useUIStream(\n    {\n      api: `${API_BASE}/api/generate`,\n      fetch,\n      validate: true,\n      onError: (err) => console.error(\"Generation error:\", err),\n    },\n  );\n\n  const handleGenerate = async () => {\n    if (!prompt.trim() || isStreaming) return;\n\n    const text = prompt.trim();\n    setPrompt(\"\");\n    Keyboard.dismiss();\n\n    await send(text, {\n      previousSpec: spec ?? undefined,\n    });\n  };\n\n  type ViewMode = \"ui\" | \"json\" | \"jsonl\";\n  const [viewMode, setViewMode] = useState<ViewMode>(\"ui\");\n  const [showMenu, setShowMenu] = useState(false);\n  const [confirmingClear, setConfirmingClear] = useState(false);\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = useCallback(async () => {\n    const text =\n      viewMode === \"jsonl\"\n        ? rawLines.join(\"\\n\")\n        : JSON.stringify(spec, null, 2);\n    if (!text) return;\n    await Clipboard.setStringAsync(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  }, [spec, rawLines, viewMode]);\n\n  const handleClear = useCallback(() => {\n    if (!confirmingClear) {\n      setConfirmingClear(true);\n      return;\n    }\n    setConfirmingClear(false);\n    setShowMenu(false);\n    clear();\n    setPrompt(\"\");\n    setViewMode(\"ui\");\n  }, [confirmingClear, clear]);\n\n  const hasContent = !!spec || isStreaming || !!error;\n\n  const PROMPT_BAR_HEIGHT = 80;\n\n  // Animate the floating prompt bar in sync with the keyboard\n  const keyboardOffset = useRef(new Animated.Value(0)).current;\n\n  useEffect(() => {\n    const showEvent =\n      Platform.OS === \"ios\" ? \"keyboardWillShow\" : \"keyboardDidShow\";\n    const hideEvent =\n      Platform.OS === \"ios\" ? \"keyboardWillHide\" : \"keyboardDidHide\";\n\n    const showSub = Keyboard.addListener(showEvent, (e) => {\n      Animated.timing(keyboardOffset, {\n        toValue: -Math.max(0, e.endCoordinates.height - insets.bottom - 4),\n        duration: Platform.OS === \"ios\" ? e.duration : 200,\n        easing: Easing.bezier(0.17, 0.59, 0.4, 0.99),\n        useNativeDriver: true,\n      }).start();\n    });\n\n    const hideSub = Keyboard.addListener(hideEvent, (e) => {\n      Animated.timing(keyboardOffset, {\n        toValue: 0,\n        duration: Platform.OS === \"ios\" ? (e.duration ?? 250) : 200,\n        easing: Easing.bezier(0.17, 0.59, 0.4, 0.99),\n        useNativeDriver: true,\n      }).start();\n    });\n\n    return () => {\n      showSub.remove();\n      hideSub.remove();\n    };\n  }, [insets.bottom, keyboardOffset]);\n\n  return (\n    <View style={styles.container}>\n      {hasContent ? (\n        viewMode !== \"ui\" ? (\n          <ScrollView\n            style={styles.flex}\n            contentContainerStyle={[\n              styles.contentContainer,\n              {\n                paddingTop: insets.top + 16,\n                paddingBottom: insets.bottom + PROMPT_BAR_HEIGHT + 16,\n              },\n            ]}\n            keyboardShouldPersistTaps=\"handled\"\n          >\n            <View style={styles.jsonContainer}>\n              <View style={styles.jsonHeader}>\n                <Text style={styles.jsonHeaderTitle}>\n                  {viewMode === \"json\" ? \"JSON Spec\" : \"JSONL Stream\"}\n                </Text>\n                <Pressable style={styles.copyButton} onPress={handleCopy}>\n                  <Text style={styles.copyButtonText}>\n                    {copied ? \"Copied\" : \"Copy\"}\n                  </Text>\n                </Pressable>\n              </View>\n              <Text style={styles.jsonText}>\n                {viewMode === \"json\"\n                  ? JSON.stringify(spec, null, 2)\n                  : rawLines.join(\"\\n\")}\n              </Text>\n            </View>\n          </ScrollView>\n        ) : (\n          <View\n            style={[\n              styles.flex,\n              { paddingBottom: insets.bottom + PROMPT_BAR_HEIGHT + 16 },\n            ]}\n          >\n            {error && (\n              <View style={styles.errorContainer}>\n                <Text style={styles.errorTitle}>Generation failed</Text>\n                <Text style={styles.errorText}>{error.message}</Text>\n              </View>\n            )}\n\n            {(spec || isStreaming) && (\n              <AppRenderer spec={spec} loading={isStreaming} />\n            )}\n          </View>\n        )\n      ) : (\n        <Pressable style={styles.splash} onPress={Keyboard.dismiss}>\n          <View style={styles.splashContent}>\n            <Text style={styles.splashTitle}>json-render</Text>\n            <Text style={styles.splashSubtitle}>\n              Describe a UI and watch it appear\n            </Text>\n            <Text style={styles.splashHint}>\n              Try something like \"a settings page with a dark mode toggle,\n              notification preferences, and a profile card with an avatar\"\n            </Text>\n          </View>\n        </Pressable>\n      )}\n\n      {/* Menu popover */}\n      {showMenu && (\n        <Pressable\n          style={styles.menuOverlay}\n          onPress={() => {\n            setShowMenu(false);\n            setConfirmingClear(false);\n          }}\n        >\n          <Animated.View\n            style={[\n              styles.menuPopover,\n              { bottom: insets.bottom + 64 },\n              { transform: [{ translateY: keyboardOffset }] },\n            ]}\n          >\n            <Pressable\n              style={styles.menuItem}\n              onPress={() => {\n                setViewMode((v) => (v === \"json\" ? \"ui\" : \"json\"));\n                setShowMenu(false);\n              }}\n            >\n              <Text style={styles.menuItemText}>\n                {viewMode === \"json\" ? \"Hide JSON\" : \"Show JSON\"}\n              </Text>\n            </Pressable>\n            <View style={styles.menuDivider} />\n            <Pressable\n              style={styles.menuItem}\n              onPress={() => {\n                setViewMode((v) => (v === \"jsonl\" ? \"ui\" : \"jsonl\"));\n                setShowMenu(false);\n              }}\n            >\n              <Text style={styles.menuItemText}>\n                {viewMode === \"jsonl\" ? \"Hide JSONL\" : \"Show JSONL\"}\n              </Text>\n            </Pressable>\n            <View style={styles.menuDivider} />\n            <Pressable style={styles.menuItem} onPress={handleClear}>\n              <Text\n                style={[\n                  styles.menuItemText,\n                  styles.menuItemDestructive,\n                  confirmingClear && styles.menuItemDestructiveConfirm,\n                ]}\n              >\n                {confirmingClear ? \"Are you sure?\" : \"Clear\"}\n              </Text>\n            </Pressable>\n          </Animated.View>\n        </Pressable>\n      )}\n\n      {/* Prompt bar */}\n      <Animated.View\n        style={[\n          styles.promptArea,\n          { bottom: insets.bottom + 12 },\n          { transform: [{ translateY: keyboardOffset }] },\n        ]}\n      >\n        <View style={styles.promptBar}>\n          <TextInput\n            style={styles.promptInput}\n            value={prompt}\n            onChangeText={setPrompt}\n            placeholder=\"Describe a UI...\"\n            placeholderTextColor=\"#9ca3af\"\n            multiline\n            maxLength={500}\n            returnKeyType=\"send\"\n            blurOnSubmit\n            onSubmitEditing={handleGenerate}\n            editable\n          />\n          {hasContent && spec && !isStreaming && (\n            <Pressable\n              style={styles.menuButton}\n              onPress={() => {\n                setShowMenu((v) => !v);\n                setConfirmingClear(false);\n              }}\n            >\n              <Text style={styles.menuButtonText}>···</Text>\n            </Pressable>\n          )}\n          {isStreaming ? (\n            <Pressable style={styles.stopButton} onPress={stop}>\n              <View style={styles.stopIcon} />\n            </Pressable>\n          ) : (\n            <Pressable\n              style={[\n                styles.sendButton,\n                !prompt.trim() && styles.sendButtonDisabled,\n              ]}\n              onPress={handleGenerate}\n              disabled={!prompt.trim()}\n            >\n              <Text style={styles.sendButtonText}>Go</Text>\n            </Pressable>\n          )}\n        </View>\n      </Animated.View>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    backgroundColor: \"#ffffff\",\n  },\n  flex: {\n    flex: 1,\n  },\n  splash: {\n    flex: 1,\n    justifyContent: \"center\",\n  },\n  splashContent: {\n    flex: 1,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    paddingHorizontal: 40,\n  },\n  splashTitle: {\n    fontSize: 32,\n    fontWeight: \"700\",\n    color: \"#111827\",\n    marginBottom: 8,\n  },\n  splashSubtitle: {\n    fontSize: 16,\n    color: \"#6b7280\",\n    marginBottom: 24,\n  },\n  splashHint: {\n    fontSize: 14,\n    color: \"#9ca3af\",\n    textAlign: \"center\",\n    lineHeight: 20,\n  },\n  contentContainer: {},\n  jsonContainer: {\n    margin: 16,\n    padding: 16,\n    backgroundColor: \"#f9fafb\",\n    borderRadius: 12,\n    borderWidth: 1,\n    borderColor: \"#e5e7eb\",\n  },\n  jsonHeader: {\n    flexDirection: \"row\",\n    justifyContent: \"space-between\",\n    alignItems: \"center\",\n    marginBottom: 12,\n  },\n  jsonHeaderTitle: {\n    fontSize: 13,\n    fontWeight: \"600\",\n    color: \"#9ca3af\",\n    textTransform: \"uppercase\",\n    letterSpacing: 0.5,\n  },\n  copyButton: {\n    backgroundColor: \"#e5e7eb\",\n    borderRadius: 8,\n    paddingHorizontal: 12,\n    paddingVertical: 6,\n  },\n  copyButtonText: {\n    fontSize: 13,\n    fontWeight: \"600\",\n    color: \"#374151\",\n  },\n  jsonText: {\n    fontSize: 12,\n    fontFamily: Platform.OS === \"ios\" ? \"Menlo\" : \"monospace\",\n    color: \"#374151\",\n    lineHeight: 18,\n  },\n  errorContainer: {\n    backgroundColor: \"#fef2f2\",\n    borderRadius: 12,\n    padding: 16,\n    margin: 16,\n    borderWidth: 1,\n    borderColor: \"#fecaca\",\n  },\n  errorTitle: {\n    fontSize: 16,\n    fontWeight: \"600\",\n    color: \"#dc2626\",\n    marginBottom: 4,\n  },\n  errorText: {\n    fontSize: 14,\n    color: \"#991b1b\",\n  },\n  promptArea: {\n    position: \"absolute\",\n    left: 12,\n    right: 12,\n  },\n  menuButton: {\n    borderRadius: 20,\n    width: 38,\n    height: 38,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    alignSelf: \"center\",\n  },\n  menuButtonText: {\n    fontSize: 18,\n    color: \"#9ca3af\",\n    fontWeight: \"700\",\n  },\n  menuOverlay: {\n    ...StyleSheet.absoluteFillObject,\n    zIndex: 10,\n  },\n  menuPopover: {\n    position: \"absolute\",\n    right: 12,\n    backgroundColor: \"#ffffff\",\n    borderRadius: 14,\n    borderWidth: 1,\n    borderColor: \"#e5e7eb\",\n    shadowColor: \"#000\",\n    shadowOffset: { width: 0, height: 4 },\n    shadowOpacity: 0.15,\n    shadowRadius: 12,\n    elevation: 10,\n    minWidth: 160,\n    overflow: \"hidden\",\n  },\n  menuItem: {\n    paddingHorizontal: 16,\n    paddingVertical: 12,\n  },\n  menuItemText: {\n    fontSize: 15,\n    color: \"#111827\",\n    fontWeight: \"500\",\n  },\n  menuItemDestructive: {\n    color: \"#ef4444\",\n  },\n  menuItemDestructiveConfirm: {\n    fontWeight: \"700\",\n  },\n  menuDivider: {\n    height: StyleSheet.hairlineWidth,\n    backgroundColor: \"#e5e7eb\",\n  },\n  promptBar: {\n    flexDirection: \"row\",\n    alignItems: \"flex-end\",\n    backgroundColor: \"#ffffff\",\n    borderRadius: 24,\n    borderWidth: 1,\n    borderColor: \"#e5e7eb\",\n    paddingLeft: 18,\n    paddingRight: 4,\n    paddingVertical: 4,\n    shadowColor: \"#000\",\n    shadowOffset: { width: 0, height: 4 },\n    shadowOpacity: 0.12,\n    shadowRadius: 16,\n    elevation: 8,\n  },\n  promptInput: {\n    flex: 1,\n    fontSize: 16,\n    color: \"#111827\",\n    paddingVertical: 10,\n    maxHeight: 100,\n  },\n  sendButton: {\n    backgroundColor: \"#3b82f6\",\n    borderRadius: 20,\n    paddingHorizontal: 18,\n    height: 38,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  sendButtonDisabled: {\n    opacity: 0.4,\n  },\n  sendButtonText: {\n    color: \"#ffffff\",\n    fontSize: 15,\n    fontWeight: \"600\",\n  },\n  stopButton: {\n    backgroundColor: \"#ef4444\",\n    borderRadius: 20,\n    width: 38,\n    height: 38,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  stopIcon: {\n    width: 14,\n    height: 14,\n    backgroundColor: \"#ffffff\",\n    borderRadius: 2,\n  },\n});\n"
  },
  {
    "path": "examples/react-native/app.json",
    "content": "{\n  \"expo\": {\n    \"name\": \"json-render-rn\",\n    \"slug\": \"json-render-rn\",\n    \"version\": \"1.0.0\",\n    \"orientation\": \"portrait\",\n    \"scheme\": \"json-render-rn\",\n    \"userInterfaceStyle\": \"light\",\n    \"newArchEnabled\": true,\n    \"ios\": {\n      \"supportsTablet\": true,\n      \"bundleIdentifier\": \"com.example.jsonrenderrn\"\n    },\n    \"android\": {\n      \"adaptiveIcon\": {\n        \"backgroundColor\": \"#ffffff\"\n      },\n      \"package\": \"com.example.jsonrenderrn\"\n    },\n    \"web\": {\n      \"bundler\": \"metro\",\n      \"output\": \"server\"\n    },\n    \"plugins\": [\n      \"expo-router\"\n    ]\n  }\n}\n"
  },
  {
    "path": "examples/react-native/lib/render/catalog.ts",
    "content": "import { z } from \"zod\";\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\";\n\n/**\n * App-specific custom rules for the AI.\n *\n * Generic React Native rules (layout, tabs, navigation, element integrity)\n * are baked into the @json-render/react-native schema as defaultRules.\n * Core concepts (initial state, $state, Repeat, pushState/removeState)\n * are covered by the core system prompt.\n *\n * Only app-specific preferences belong here.\n */\nexport const customRules = [\n  // Placeholder images using Picsum (free, no API key)\n  'Image props: { \"src\": \"https://picsum.photos/WIDTH/HEIGHT?random=N\" } - use Picsum for any placeholder or example images',\n  \"Use different random numbers for each image to get different photos (e.g., ?random=1, ?random=2, ?random=3)\",\n  \"Picsum provides random professional stock photos - great for avatars, hero images, product shots, and backgrounds\",\n  'Avatar props: { \"src\": \"https://picsum.photos/100/100?random=N\" } - use Picsum for avatar images too',\n\n  // Icons vs emojis (app uses Ionicons via the custom Icon component)\n  \"CRITICAL: NEVER use emoji characters for UI icons, action buttons, navigation items, or indicators. ALWAYS use the Icon component instead.\",\n  \"The Icon component uses Ionicons. Common icon names: heart, heart-outline, chatbubble-outline, share-social-outline, bookmark-outline, bookmark, home, home-outline, search, person, person-outline, add, close, checkmark, ellipsis-horizontal, ellipsis-vertical, camera-outline, notifications-outline, settings-outline, send, arrow-back, arrow-forward, chevron-back, chevron-forward, star, star-outline, eye-outline, eye-off-outline, trash-outline, create-outline, refresh, lock-closed-outline, mail-outline, call-outline, location-outline, time-outline, play, pause, image-outline, menu, filter-outline, globe-outline, link-outline, cloud-outline, download-outline, share-outline.\",\n  \"Emojis ARE allowed inside user-generated content such as comment text, post captions, chat messages, and status text - just like real social apps. Only UI chrome (buttons, tabs, indicators) must use the Icon component.\",\n];\n\n/**\n * React Native catalog\n *\n * Uses all standard components and actions from @json-render/react-native,\n * plus an Icon component powered by Ionicons (@expo/vector-icons).\n */\nexport const catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Icon: {\n      props: z.object({\n        name: z.string(),\n        size: z.number().nullable(),\n        color: z.string().nullable(),\n      }),\n      slots: [],\n      description:\n        \"Icon display using Ionicons. Use for action buttons, navigation items, and indicators. ALWAYS use this instead of emoji characters for UI icons. Use Ionicons naming convention (e.g. heart, heart-outline, chatbubble-outline, share-social-outline).\",\n      example: { name: \"heart-outline\", size: 24, color: \"#007AFF\" },\n    },\n  },\n  actions: standardActionDefinitions,\n});\n"
  },
  {
    "path": "examples/react-native/lib/render/registry.tsx",
    "content": "import React from \"react\";\nimport Ionicons from \"@expo/vector-icons/Ionicons\";\nimport { defineRegistry, type Components } from \"@json-render/react-native\";\nimport { catalog } from \"./catalog\";\n\n// =============================================================================\n// Registry\n// =============================================================================\n\n/**\n * Custom component registry using defineRegistry.\n *\n * Only custom components need to be defined here — standard React Native\n * components (Container, Row, Column, Button, etc.) are included automatically\n * by the Renderer via `includeStandard`.\n */\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => (\n      <Ionicons\n        name={props.name as keyof typeof Ionicons.glyphMap}\n        size={props.size ?? 24}\n        color={props.color ?? \"#111827\"}\n      />\n    ),\n  } as Components<typeof catalog>,\n});\n"
  },
  {
    "path": "examples/react-native/lib/render/renderer.tsx",
    "content": "import React, { type ReactNode, useMemo } from \"react\";\nimport {\n  Renderer,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n  ValidationProvider,\n  createStandardActionHandlers,\n  type Spec,\n} from \"@json-render/react-native\";\nimport { registry } from \"./registry\";\n\n// =============================================================================\n// AppRenderer\n// =============================================================================\n\ninterface AppRendererProps {\n  spec: Spec | null;\n  loading?: boolean;\n}\n\nexport function AppRenderer({ spec, loading }: AppRendererProps): ReactNode {\n  // Seed the StateProvider with any initial state from the spec.\n  // Memoize so we only pick up the state from the first render of this spec\n  // (otherwise re-renders during streaming would keep resetting the state).\n  // NOTE: This hook must be called before any early return to satisfy Rules of Hooks.\n  const initialState = useMemo(\n    () => spec?.state ?? {},\n    // Re-seed when the spec root changes (new generation)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [spec?.root],\n  );\n\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={initialState}>\n      <VisibilityProvider>\n        <ActionProvider handlers={createStandardActionHandlers()}>\n          <ValidationProvider>\n            <Renderer spec={spec} registry={registry} loading={loading} />\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/react-native/metro.config.js",
    "content": "const { getDefaultConfig } = require(\"expo/metro-config\");\nconst path = require(\"path\");\n\n// Monorepo root\nconst workspaceRoot = path.resolve(__dirname, \"../..\");\nconst projectRoot = __dirname;\n\nconst config = getDefaultConfig(projectRoot);\n\n// Watch the entire monorepo so changes in packages/ are picked up\nconfig.watchFolders = [workspaceRoot];\n\n// Resolve modules from both the project and monorepo root\nconfig.resolver.nodeModulesPaths = [\n  path.resolve(projectRoot, \"node_modules\"),\n  path.resolve(workspaceRoot, \"node_modules\"),\n];\n\n// Force shared dependencies to always resolve from the project's node_modules.\n// This is necessary in pnpm monorepos where Metro follows symlinks into .pnpm/\n// and then resolves peer deps from the wrong location.\nconst forcedDeps = {\n  react: require.resolve(\"react\", { paths: [projectRoot] }),\n  \"react-native\": require.resolve(\"react-native\", { paths: [projectRoot] }),\n};\n\nconst originalResolveRequest = config.resolver.resolveRequest;\n\nconfig.resolver.resolveRequest = (context, moduleName, platform) => {\n  // Intercept imports of shared deps and force them to the project's copy\n  if (forcedDeps[moduleName]) {\n    return {\n      filePath: forcedDeps[moduleName],\n      type: \"sourceFile\",\n    };\n  }\n\n  // Fall back to default resolution\n  if (originalResolveRequest) {\n    return originalResolveRequest(context, moduleName, platform);\n  }\n  return context.resolveRequest(context, moduleName, platform);\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "examples/react-native/package.json",
    "content": "{\n  \"name\": \"example-react-native\",\n  \"version\": \"0.1.9\",\n  \"private\": true,\n  \"main\": \"expo-router/entry\",\n  \"scripts\": {\n    \"start\": \"expo start\",\n    \"ios\": \"expo start --ios\",\n    \"android\": \"expo start --android\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.39\",\n    \"@expo/vector-icons\": \"^15.0.3\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react-native\": \"workspace:*\",\n    \"ai\": \"^6.0.77\",\n    \"expo\": \"~54.0.33\",\n    \"expo-clipboard\": \"~8.0.8\",\n    \"expo-constants\": \"~18.0.4\",\n    \"expo-router\": \"~6.0.23\",\n    \"expo-status-bar\": \"~2.2.3\",\n    \"react\": \"19.1.0\",\n    \"react-freeze\": \"^1.0.4\",\n    \"react-native\": \"0.81.4\",\n    \"react-native-safe-area-context\": \"~5.4.0\",\n    \"react-native-screens\": \"~4.11.1\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"~19.1.0\",\n    \"typescript\": \"~5.8.3\"\n  }\n}\n"
  },
  {
    "path": "examples/react-native/tsconfig.json",
    "content": "{\n  \"extends\": \"expo/tsconfig.base\",\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "examples/react-pdf/.env.example",
    "content": "# Required: AI Gateway API key\nAI_GATEWAY_API_KEY=\n\n# Optional: override the default model (default: anthropic/claude-haiku-4.5)\n# AI_GATEWAY_MODEL=openai/gpt-4o-mini\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/react-pdf/CHANGELOG.md",
    "content": "# example-react-pdf\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react-pdf@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react-pdf@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react-pdf@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react-pdf@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react-pdf@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react-pdf@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react-pdf@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react-pdf@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react-pdf@0.9.0\n"
  },
  {
    "path": "examples/react-pdf/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { gateway } from \"@ai-sdk/gateway\";\nimport { buildUserPrompt, type Spec } from \"@json-render/core\";\nimport { pdfCatalog } from \"@/lib/catalog\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 60;\n\nconst SYSTEM_PROMPT = pdfCatalog.prompt();\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt, startingSpec } = (await req.json()) as {\n    prompt: string;\n    startingSpec?: Spec | null;\n  };\n\n  if (!prompt || typeof prompt !== \"string\") {\n    return Response.json({ error: \"prompt is required\" }, { status: 400 });\n  }\n\n  const userPrompt = buildUserPrompt({\n    prompt,\n    currentSpec: startingSpec,\n  });\n\n  const result = streamText({\n    model: gateway(process.env.AI_GATEWAY_MODEL ?? DEFAULT_MODEL),\n    system: SYSTEM_PROMPT,\n    prompt: userPrompt,\n    temperature: 0.7,\n  });\n\n  return result.toTextStreamResponse();\n}\n"
  },
  {
    "path": "examples/react-pdf/app/api/pdf/route.ts",
    "content": "import { renderToBuffer } from \"@json-render/react-pdf/render\";\nimport { examples } from \"@/lib/examples\";\nimport type { Spec } from \"@json-render/core\";\n\nexport async function GET(req: Request) {\n  const { searchParams } = new URL(req.url);\n  const name = searchParams.get(\"name\") ?? \"invoice\";\n  const download = searchParams.get(\"download\") === \"1\";\n\n  const example = examples.find((e) => e.name === name);\n  if (!example) {\n    return new Response(\"Example not found\", { status: 404 });\n  }\n\n  return pdfResponse(example.spec, name, download);\n}\n\nexport async function POST(req: Request) {\n  const { spec, download, filename } = (await req.json()) as {\n    spec: Spec;\n    download?: boolean;\n    filename?: string;\n  };\n\n  if (!spec || !spec.root || !spec.elements) {\n    return new Response(\"Invalid spec\", { status: 400 });\n  }\n\n  return pdfResponse(spec, filename ?? \"document\", download ?? false);\n}\n\nasync function pdfResponse(spec: Spec, name: string, download: boolean) {\n  const buffer = await renderToBuffer(spec);\n\n  const disposition = download\n    ? `attachment; filename=\"${name}.pdf\"`\n    : `inline; filename=\"${name}.pdf\"`;\n\n  return new Response(buffer as unknown as ArrayBuffer, {\n    headers: {\n      \"Content-Type\": \"application/pdf\",\n      \"Content-Disposition\": disposition,\n      \"Cache-Control\": \"no-store\",\n    },\n  });\n}\n"
  },
  {
    "path": "examples/react-pdf/app/globals.css",
    "content": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n    --radius-2xl: calc(var(--radius) + 8px);\n    --radius-3xl: calc(var(--radius) + 12px);\n    --radius-4xl: calc(var(--radius) + 16px);\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n}\n\n:root {\n    --radius: 0.625rem;\n    --background: oklch(1 0 0);\n    --foreground: oklch(0.145 0 0);\n    --card: oklch(1 0 0);\n    --card-foreground: oklch(0.145 0 0);\n    --popover: oklch(1 0 0);\n    --popover-foreground: oklch(0.145 0 0);\n    --primary: oklch(0.205 0 0);\n    --primary-foreground: oklch(0.985 0 0);\n    --secondary: oklch(0.97 0 0);\n    --secondary-foreground: oklch(0.205 0 0);\n    --muted: oklch(0.97 0 0);\n    --muted-foreground: oklch(0.556 0 0);\n    --accent: oklch(0.97 0 0);\n    --accent-foreground: oklch(0.205 0 0);\n    --destructive: oklch(0.577 0.245 27.325);\n    --border: oklch(0.922 0 0);\n    --input: oklch(0.922 0 0);\n    --ring: oklch(0.708 0 0);\n    --chart-1: oklch(0.646 0.222 41.116);\n    --chart-2: oklch(0.6 0.118 184.704);\n    --chart-3: oklch(0.398 0.07 227.392);\n    --chart-4: oklch(0.828 0.189 84.429);\n    --chart-5: oklch(0.769 0.188 70.08);\n    --sidebar: oklch(0.985 0 0);\n    --sidebar-foreground: oklch(0.145 0 0);\n    --sidebar-primary: oklch(0.205 0 0);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.97 0 0);\n    --sidebar-accent-foreground: oklch(0.205 0 0);\n    --sidebar-border: oklch(0.922 0 0);\n    --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n    --background: oklch(0.145 0 0);\n    --foreground: oklch(0.985 0 0);\n    --card: oklch(0.205 0 0);\n    --card-foreground: oklch(0.985 0 0);\n    --popover: oklch(0.205 0 0);\n    --popover-foreground: oklch(0.985 0 0);\n    --primary: oklch(0.922 0 0);\n    --primary-foreground: oklch(0.205 0 0);\n    --secondary: oklch(0.269 0 0);\n    --secondary-foreground: oklch(0.985 0 0);\n    --muted: oklch(0.269 0 0);\n    --muted-foreground: oklch(0.708 0 0);\n    --accent: oklch(0.269 0 0);\n    --accent-foreground: oklch(0.985 0 0);\n    --destructive: oklch(0.704 0.191 22.216);\n    --border: oklch(1 0 0 / 10%);\n    --input: oklch(1 0 0 / 15%);\n    --ring: oklch(0.556 0 0);\n    --chart-1: oklch(0.488 0.243 264.376);\n    --chart-2: oklch(0.696 0.17 162.48);\n    --chart-3: oklch(0.769 0.188 70.08);\n    --chart-4: oklch(0.627 0.265 303.9);\n    --chart-5: oklch(0.645 0.246 16.439);\n    --sidebar: oklch(0.205 0 0);\n    --sidebar-foreground: oklch(0.985 0 0);\n    --sidebar-primary: oklch(0.488 0.243 264.376);\n    --sidebar-primary-foreground: oklch(0.985 0 0);\n    --sidebar-accent: oklch(0.269 0 0);\n    --sidebar-accent-foreground: oklch(0.985 0 0);\n    --sidebar-border: oklch(1 0 0 / 10%);\n    --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}"
  },
  {
    "path": "examples/react-pdf/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"json-render React PDF Example\",\n  description:\n    \"Generate PDF documents from JSON specs with @json-render/react-pdf\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body className={inter.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/react-pdf/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { examples } from \"@/lib/examples\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport type { Spec } from \"@json-render/core\";\nimport { cn } from \"@/lib/utils\";\n\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Sheet, SheetContent, SheetTitle } from \"@/components/ui/sheet\";\nimport {\n  ResizablePanelGroup,\n  ResizablePanel,\n  ResizableHandle,\n} from \"@/components/ui/resizable\";\nimport { FileText, Download, Loader2, ArrowRight, Square } from \"lucide-react\";\n\ntype Mode = \"scratch\" | \"example\";\ntype MobileView = \"json\" | \"preview\";\n\ninterface Selection {\n  mode: Mode;\n  exampleName?: string;\n}\n\nconst PDF_REFRESH_INTERVAL_MS = 2000;\n\nfunction CopyButton({ text }: { text: string }) {\n  const [copied, setCopied] = useState(false);\n\n  return (\n    <button\n      onClick={() => {\n        navigator.clipboard.writeText(text);\n        setCopied(true);\n        setTimeout(() => setCopied(false), 1500);\n      }}\n      className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono\"\n    >\n      {copied ? \"copied\" : \"copy\"}\n    </button>\n  );\n}\n\nfunction isRenderableSpec(spec: Spec | null): spec is Spec {\n  if (!spec?.root || !spec.elements) return false;\n  const root = spec.elements[spec.root];\n  if (!root) return false;\n  if (root.type !== \"Document\" || !root.children?.length) return false;\n  const firstChild = spec.elements[root.children[0]!];\n  return firstChild?.type === \"Page\";\n}\n\nexport default function Page() {\n  const [selection, setSelection] = useState<Selection>({\n    mode: \"example\",\n    exampleName: examples[0]!.name,\n  });\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n  const [generatedSpec, setGeneratedSpec] = useState<Spec | null>(null);\n  const [pdfUrl, setPdfUrl] = useState<string | null>(null);\n  const [error, setError] = useState<string | null>(null);\n  const [mobileView, setMobileView] = useState<MobileView>(\"preview\");\n  const [examplesSheetOpen, setExamplesSheetOpen] = useState(false);\n  const [refreshing, setRefreshing] = useState(false);\n  const pdfUrlRef = useRef<string | null>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const mobileInputRef = useRef<HTMLTextAreaElement>(null);\n  const abortRef = useRef<AbortController | null>(null);\n  const codeScrollRef = useRef<HTMLDivElement>(null);\n  const mobileCodeScrollRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!generating) return;\n    codeScrollRef.current?.scrollTo({\n      top: codeScrollRef.current.scrollHeight,\n    });\n    mobileCodeScrollRef.current?.scrollTo({\n      top: mobileCodeScrollRef.current.scrollHeight,\n    });\n  }, [generating, generatedSpec]);\n\n  const currentExample =\n    selection.mode === \"example\"\n      ? examples.find((e) => e.name === selection.exampleName)\n      : null;\n\n  const activeSpec = generatedSpec ?? currentExample?.spec ?? null;\n\n  const examplePdfUrl =\n    selection.mode === \"example\" && !generatedSpec\n      ? `/api/pdf?name=${selection.exampleName}`\n      : null;\n\n  const displayPdfUrl = pdfUrl ?? examplePdfUrl;\n\n  useEffect(() => {\n    inputRef.current?.focus();\n  }, [selection.mode, selection.exampleName]);\n\n  const fetchPdfBlob = useCallback(async (spec: Spec, signal?: AbortSignal) => {\n    const res = await fetch(\"/api/pdf\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ spec }),\n      signal,\n    });\n    if (!res.ok) throw new Error(\"Failed to generate PDF\");\n    const blob = await res.blob();\n    const url = URL.createObjectURL(blob);\n\n    const prev = pdfUrlRef.current;\n    pdfUrlRef.current = url;\n    setPdfUrl(url);\n\n    if (prev) URL.revokeObjectURL(prev);\n  }, []);\n\n  // Progressive PDF refresh during generation\n  const lastRefreshSpec = useRef<string>(\"\");\n  const generatedSpecRef = useRef<Spec | null>(null);\n  generatedSpecRef.current = generatedSpec;\n\n  useEffect(() => {\n    if (!generating) return;\n\n    const interval = setInterval(() => {\n      const spec = generatedSpecRef.current;\n      if (!spec) return;\n\n      const specKey = JSON.stringify(spec);\n      if (specKey === lastRefreshSpec.current) return;\n      if (!isRenderableSpec(spec)) return;\n\n      lastRefreshSpec.current = specKey;\n      setRefreshing(true);\n      fetchPdfBlob(spec)\n        .catch(() => {})\n        .finally(() => setRefreshing(false));\n    }, PDF_REFRESH_INTERVAL_MS);\n\n    return () => clearInterval(interval);\n  }, [generating, fetchPdfBlob]);\n\n  const handleGenerate = useCallback(async () => {\n    if (!prompt.trim()) return;\n\n    abortRef.current?.abort();\n    const controller = new AbortController();\n    abortRef.current = controller;\n\n    setGenerating(true);\n    setError(null);\n    lastRefreshSpec.current = \"\";\n\n    try {\n      const startingSpec =\n        selection.mode === \"example\" && currentExample\n          ? currentExample.spec\n          : null;\n\n      const res = await fetch(\"/api/generate\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt: prompt.trim(), startingSpec }),\n        signal: controller.signal,\n      });\n      if (!res.ok) throw new Error(\"Generation failed\");\n\n      const reader = res.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n      const compiler = createSpecStreamCompiler<Spec>(\n        startingSpec ? { ...startingSpec } : {},\n      );\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        const chunk = decoder.decode(value, { stream: true });\n        const { result, newPatches } = compiler.push(chunk);\n        if (newPatches.length > 0) setGeneratedSpec(result);\n      }\n\n      const finalSpec = compiler.getResult();\n      setGeneratedSpec(finalSpec);\n      setGenerating(false);\n\n      await fetchPdfBlob(finalSpec);\n    } catch (e) {\n      if (controller.signal.aborted) return;\n      setError(e instanceof Error ? e.message : \"Something went wrong\");\n      setGenerating(false);\n    }\n  }, [prompt, selection, currentExample, fetchPdfBlob]);\n\n  const handleStop = useCallback(() => {\n    abortRef.current?.abort();\n    setGenerating(false);\n\n    if (isRenderableSpec(generatedSpec)) {\n      fetchPdfBlob(generatedSpec).catch(() => {});\n    }\n  }, [generatedSpec, fetchPdfBlob]);\n\n  const select = (next: Selection) => {\n    abortRef.current?.abort();\n    setSelection(next);\n    setGeneratedSpec(null);\n    setPdfUrl(null);\n    setError(null);\n    setPrompt(\"\");\n    setGenerating(false);\n    setExamplesSheetOpen(false);\n  };\n\n  const handleDownload = async () => {\n    if (!activeSpec) return;\n    if (generatedSpec) {\n      const res = await fetch(\"/api/pdf\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ spec: generatedSpec, download: true }),\n      });\n      const blob = await res.blob();\n      const url = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = \"document.pdf\";\n      a.click();\n      URL.revokeObjectURL(url);\n    } else if (selection.mode === \"example\") {\n      window.open(\n        `/api/pdf?name=${selection.exampleName}&download=1`,\n        \"_blank\",\n      );\n    }\n  };\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\" && !e.shiftKey) {\n        e.preventDefault();\n        handleGenerate();\n      }\n    },\n    [handleGenerate],\n  );\n\n  const jsonCode = activeSpec\n    ? JSON.stringify(activeSpec, null, 2)\n    : \"// select an example or generate a PDF\";\n\n  // ---------------------------------------------------------------------------\n  // Pane: Chat / Examples\n  // ---------------------------------------------------------------------------\n  const chatPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-2\">\n        <FileText className=\"h-3.5 w-3.5 text-muted-foreground\" />\n        <span className=\"text-xs font-mono text-muted-foreground\">\n          json-render / react-pdf\n        </span>\n      </div>\n\n      <ScrollArea className=\"flex-1\">\n        <div className=\"p-2 space-y-1\">\n          <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n            start\n          </p>\n          <button\n            onClick={() => select({ mode: \"scratch\" })}\n            className={cn(\n              \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n              selection.mode === \"scratch\"\n                ? \"bg-muted text-foreground\"\n                : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n            )}\n          >\n            <span className=\"font-medium\">From scratch</span>\n          </button>\n\n          <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n            examples\n          </p>\n          {examples.map((ex) => (\n            <button\n              key={ex.name}\n              onClick={() => select({ mode: \"example\", exampleName: ex.name })}\n              className={cn(\n                \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                selection.mode === \"example\" &&\n                  selection.exampleName === ex.name\n                  ? \"bg-muted text-foreground\"\n                  : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n              )}\n            >\n              <span className=\"font-medium\">{ex.label}</span>\n              <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                {ex.description}\n              </p>\n            </button>\n          ))}\n        </div>\n      </ScrollArea>\n\n      <div\n        className=\"border-t border-border p-3 cursor-text\"\n        onMouseDown={(e) => {\n          const target = e.target as HTMLElement;\n          if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n            e.preventDefault();\n            inputRef.current?.focus();\n          }\n        }}\n      >\n        {error && (\n          <div className=\"mb-2 rounded bg-destructive/10 px-3 py-1.5 text-xs text-destructive\">\n            {error}\n          </div>\n        )}\n        <textarea\n          ref={inputRef}\n          value={prompt}\n          onChange={(e) => setPrompt(e.target.value)}\n          onKeyDown={handleKeyDown}\n          placeholder={\n            selection.mode === \"scratch\"\n              ? \"Describe the PDF you want...\"\n              : `Modify the ${currentExample?.label ?? \"example\"}...`\n          }\n          className=\"w-full bg-background text-sm resize-none outline-none placeholder:text-muted-foreground/50\"\n          rows={2}\n          autoFocus\n        />\n        <div className=\"flex justify-between items-center mt-2\">\n          <span className=\"text-[11px] text-muted-foreground\">\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </span>\n          {generating ? (\n            <button\n              onClick={handleStop}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n              aria-label=\"Stop\"\n            >\n              <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n            </button>\n          ) : (\n            <button\n              onClick={handleGenerate}\n              disabled={!prompt.trim()}\n              className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n              aria-label=\"Generate\"\n            >\n              <ArrowRight className=\"h-3.5 w-3.5\" />\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: JSON Spec\n  // ---------------------------------------------------------------------------\n  const codePane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">json</span>\n        {generating && (\n          <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && <CopyButton text={jsonCode} />}\n      </div>\n      <div ref={codeScrollRef} className=\"flex-1 overflow-auto\">\n        <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n          {jsonCode}\n        </pre>\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Pane: PDF Preview\n  // ---------------------------------------------------------------------------\n  const previewPane = (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"border-b border-border px-3 h-9 flex items-center gap-3\">\n        <span className=\"text-xs font-mono text-foreground\">preview</span>\n        {(generating || refreshing) && (\n          <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin\" />\n        )}\n        <div className=\"flex-1\" />\n        {activeSpec && (\n          <button\n            onClick={handleDownload}\n            className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1\"\n          >\n            <Download className=\"h-3 w-3\" />\n            download\n          </button>\n        )}\n      </div>\n      <div className=\"flex-1 relative bg-neutral-600\">\n        {displayPdfUrl ? (\n          <iframe\n            key={displayPdfUrl}\n            src={displayPdfUrl}\n            className=\"h-full w-full border-none\"\n            title=\"PDF preview\"\n          />\n        ) : (\n          <div className=\"h-full flex flex-col items-center justify-center gap-2 text-neutral-400\">\n            <FileText className=\"h-10 w-10\" />\n            <p className=\"text-sm\">\n              {selection.mode === \"scratch\"\n                ? \"Enter a prompt to generate a PDF\"\n                : \"Select an example to preview\"}\n            </p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Render\n  // ---------------------------------------------------------------------------\n  return (\n    <div className=\"h-dvh flex flex-col\">\n      {/* Desktop: 3-pane resizable layout */}\n      <div className=\"hidden lg:flex flex-1 min-h-0\">\n        <ResizablePanelGroup className=\"flex-1\">\n          <ResizablePanel defaultSize={25} minSize={15}>\n            {chatPane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={35} minSize={20}>\n            {codePane}\n          </ResizablePanel>\n          <ResizableHandle />\n          <ResizablePanel defaultSize={40} minSize={20}>\n            {previewPane}\n          </ResizablePanel>\n        </ResizablePanelGroup>\n      </div>\n\n      {/* Mobile: toolbar + content + prompt */}\n      <div className=\"flex lg:hidden flex-col flex-1 min-h-0\">\n        <div className=\"border-b border-border px-3 h-9 flex items-center gap-3 shrink-0\">\n          <button\n            onClick={() => setExamplesSheetOpen(true)}\n            className=\"text-xs font-mono font-medium px-1.5 py-0.5 rounded bg-muted text-foreground shrink-0\"\n          >\n            {selection.mode === \"example\" && currentExample\n              ? currentExample.label\n              : \"scratch\"}\n          </button>\n          {([\"json\", \"preview\"] as const).map((tab) => (\n            <button\n              key={tab}\n              onClick={() => setMobileView(tab)}\n              className={cn(\n                \"text-xs font-mono transition-colors shrink-0\",\n                mobileView === tab\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground hover:text-foreground\",\n              )}\n            >\n              {tab}\n            </button>\n          ))}\n          {(generating || refreshing) && (\n            <Loader2 className=\"h-3 w-3 text-muted-foreground animate-spin shrink-0\" />\n          )}\n          <div className=\"flex-1\" />\n          {activeSpec && (\n            <button\n              onClick={handleDownload}\n              className=\"text-xs text-muted-foreground hover:text-foreground transition-colors font-mono flex items-center gap-1\"\n            >\n              <Download className=\"h-3 w-3\" />\n            </button>\n          )}\n        </div>\n\n        <div ref={mobileCodeScrollRef} className=\"flex-1 min-h-0 overflow-auto\">\n          {mobileView === \"json\" ? (\n            <pre className=\"p-3 text-xs leading-relaxed font-mono text-muted-foreground whitespace-pre\">\n              {jsonCode}\n            </pre>\n          ) : (\n            <div className=\"h-full relative bg-neutral-600\">\n              {displayPdfUrl ? (\n                <iframe\n                  key={displayPdfUrl}\n                  src={displayPdfUrl}\n                  className=\"h-full w-full border-none\"\n                  title=\"PDF preview\"\n                />\n              ) : (\n                <div className=\"h-full flex flex-col items-center justify-center gap-2 text-neutral-400\">\n                  <FileText className=\"h-10 w-10\" />\n                  <p className=\"text-sm\">Enter a prompt to generate a PDF</p>\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n\n        <div\n          className=\"border-t border-border p-3 shrink-0 cursor-text\"\n          onMouseDown={(e) => {\n            const target = e.target as HTMLElement;\n            if (!target.closest(\"button\") && target.tagName !== \"TEXTAREA\") {\n              e.preventDefault();\n              mobileInputRef.current?.focus();\n            }\n          }}\n        >\n          <textarea\n            ref={mobileInputRef}\n            value={prompt}\n            onChange={(e) => setPrompt(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder={\n              selection.mode === \"scratch\"\n                ? \"Describe the PDF you want...\"\n                : `Modify the ${currentExample?.label ?? \"example\"}...`\n            }\n            className=\"w-full bg-background text-base resize-none outline-none placeholder:text-muted-foreground/50\"\n            rows={2}\n          />\n          <div className=\"flex justify-between items-center mt-2\">\n            <span className=\"text-[11px] text-muted-foreground\">\n              {selection.mode === \"example\" && currentExample\n                ? currentExample.label\n                : \"scratch\"}\n            </span>\n            {generating ? (\n              <button\n                onClick={handleStop}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n                aria-label=\"Stop\"\n              >\n                <Square className=\"h-3 w-3\" fill=\"currentColor\" />\n              </button>\n            ) : (\n              <button\n                onClick={handleGenerate}\n                disabled={!prompt.trim()}\n                className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n                aria-label=\"Generate\"\n              >\n                <ArrowRight className=\"h-3.5 w-3.5\" />\n              </button>\n            )}\n          </div>\n        </div>\n\n        <Sheet open={examplesSheetOpen} onOpenChange={setExamplesSheetOpen}>\n          <SheetContent side=\"left\" className=\"w-80 p-0\">\n            <SheetTitle className=\"sr-only\">Examples</SheetTitle>\n            <ScrollArea className=\"h-full\">\n              <div className=\"p-2 space-y-1\">\n                <p className=\"px-2 pt-2 pb-1 text-[11px] font-mono text-muted-foreground\">\n                  start\n                </p>\n                <button\n                  onClick={() => select({ mode: \"scratch\" })}\n                  className={cn(\n                    \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                    selection.mode === \"scratch\"\n                      ? \"bg-muted text-foreground\"\n                      : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                  )}\n                >\n                  From scratch\n                </button>\n                <p className=\"px-2 pt-3 pb-1 text-[11px] font-mono text-muted-foreground\">\n                  examples\n                </p>\n                {examples.map((ex) => (\n                  <button\n                    key={ex.name}\n                    onClick={() =>\n                      select({ mode: \"example\", exampleName: ex.name })\n                    }\n                    className={cn(\n                      \"w-full text-left px-3 py-2 rounded text-sm transition-colors\",\n                      selection.mode === \"example\" &&\n                        selection.exampleName === ex.name\n                        ? \"bg-muted text-foreground\"\n                        : \"text-muted-foreground hover:bg-muted/50 hover:text-foreground\",\n                    )}\n                  >\n                    <span className=\"font-medium\">{ex.label}</span>\n                    <p className=\"text-xs text-muted-foreground/70 mt-0.5 leading-snug\">\n                      {ex.description}\n                    </p>\n                  </button>\n                ))}\n              </div>\n            </ScrollArea>\n          </SheetContent>\n        </Sheet>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/react-pdf/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "examples/react-pdf/components/ui/resizable.tsx",
    "content": "\"use client\";\n\nimport { GripVerticalIcon } from \"lucide-react\";\nimport * as ResizablePrimitive from \"react-resizable-panels\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ResizablePanelGroup({\n  className,\n  ...props\n}: ResizablePrimitive.GroupProps) {\n  return (\n    <ResizablePrimitive.Group\n      data-slot=\"resizable-panel-group\"\n      className={cn(\n        \"flex h-full w-full aria-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {\n  return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />;\n}\n\nfunction ResizableHandle({\n  withHandle,\n  className,\n  ...props\n}: ResizablePrimitive.SeparatorProps & {\n  withHandle?: boolean;\n}) {\n  return (\n    <ResizablePrimitive.Separator\n      data-slot=\"resizable-handle\"\n      className={cn(\n        \"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n        className,\n      )}\n      {...props}\n    >\n      {withHandle && (\n        <div className=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n          <GripVerticalIcon className=\"size-2.5\" />\n        </div>\n      )}\n    </ResizablePrimitive.Separator>\n  );\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup };\n"
  },
  {
    "path": "examples/react-pdf/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ScrollArea({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n  return (\n    <ScrollAreaPrimitive.Root\n      data-slot=\"scroll-area\"\n      className={cn(\"relative\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        data-slot=\"scroll-area-viewport\"\n        className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  );\n}\n\nfunction ScrollBar({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n  return (\n    <ScrollAreaPrimitive.ScrollAreaScrollbar\n      data-slot=\"scroll-area-scrollbar\"\n      orientation={orientation}\n      className={cn(\n        \"flex touch-none p-px transition-colors select-none\",\n        orientation === \"vertical\" &&\n          \"h-full w-2.5 border-l border-l-transparent\",\n        orientation === \"horizontal\" &&\n          \"h-2.5 flex-col border-t border-t-transparent\",\n        className,\n      )}\n      {...props}\n    >\n      <ScrollAreaPrimitive.ScrollAreaThumb\n        data-slot=\"scroll-area-thumb\"\n        className=\"bg-border relative flex-1 rounded-full\"\n      />\n    </ScrollAreaPrimitive.ScrollAreaScrollbar>\n  );\n}\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "examples/react-pdf/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "examples/react-pdf/components/ui/sheet.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n  return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />;\n}\n\nfunction SheetTrigger({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\nfunction SheetPortal({\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n  return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />;\n}\n\nfunction SheetOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  showCloseButton?: boolean;\n}) {\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n          side === \"right\" &&\n            \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n          side === \"left\" &&\n            \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n          side === \"top\" &&\n            \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n          side === \"bottom\" &&\n            \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n            <XIcon className=\"size-4\" />\n            <span className=\"sr-only\">Close</span>\n          </SheetPrimitive.Close>\n        )}\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-header\"\n      className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n"
  },
  {
    "path": "examples/react-pdf/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst tabsListVariants = cva(\n  \"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction TabsList({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n  VariantProps<typeof tabsListVariants>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground\",\n        \"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n"
  },
  {
    "path": "examples/react-pdf/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "examples/react-pdf/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"rtl\": false,\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"registries\": {}\n}\n"
  },
  {
    "path": "examples/react-pdf/lib/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react-pdf/server\";\nimport { standardComponentDefinitions } from \"@json-render/react-pdf/catalog\";\n\nexport const pdfCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n  actions: {},\n});\n"
  },
  {
    "path": "examples/react-pdf/lib/examples.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport interface Example {\n  name: string;\n  label: string;\n  description: string;\n  spec: Spec;\n}\n\nexport const examples: Example[] = [\n  // =========================================================================\n  // Invoice\n  // =========================================================================\n  {\n    name: \"invoice\",\n    label: \"Invoice\",\n    description:\n      \"A professional invoice with company header, billing details, line items, and totals\",\n    spec: {\n      root: \"doc\",\n      elements: {\n        doc: {\n          type: \"Document\",\n          props: { title: \"Invoice #1042\", author: \"Acme Corp\", subject: null },\n          children: [\"page\"],\n        },\n        page: {\n          type: \"Page\",\n          props: {\n            size: \"A4\",\n            orientation: null,\n            marginTop: 50,\n            marginBottom: 50,\n            marginLeft: 50,\n            marginRight: 50,\n            backgroundColor: null,\n          },\n          children: [\n            \"header\",\n            \"spacer-1\",\n            \"billing-row\",\n            \"spacer-2\",\n            \"items-table\",\n            \"spacer-3\",\n            \"totals\",\n            \"spacer-4\",\n            \"footer\",\n            \"page-num\",\n          ],\n        },\n\n        // Header\n        header: {\n          type: \"Row\",\n          props: {\n            gap: null,\n            alignItems: \"flex-end\",\n            justifyContent: \"space-between\",\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"company-name\", \"invoice-info\"],\n        },\n        \"company-name\": {\n          type: \"Heading\",\n          props: {\n            text: \"Acme Corp\",\n            level: \"h1\",\n            color: \"#1a202c\",\n            align: null,\n          },\n        },\n        \"invoice-info\": {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: \"flex-end\",\n            justifyContent: null,\n            padding: null,\n            flex: null,\n          },\n          children: [\"inv-number\", \"inv-date\", \"inv-due\"],\n        },\n        \"inv-number\": {\n          type: \"Text\",\n          props: {\n            text: \"Invoice #1042\",\n            fontSize: 12,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"inv-date\": {\n          type: \"Text\",\n          props: {\n            text: \"Date: February 19, 2026\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"inv-due\": {\n          type: \"Text\",\n          props: {\n            text: \"Due: March 19, 2026\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-1\": { type: \"Spacer\", props: { height: 30 } },\n\n        // Billing info\n        \"billing-row\": {\n          type: \"Row\",\n          props: {\n            gap: 40,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"bill-from\", \"bill-to\"],\n        },\n        \"bill-from\": {\n          type: \"Column\",\n          props: {\n            gap: 4,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: 1,\n          },\n          children: [\"from-label\", \"from-name\", \"from-addr\", \"from-email\"],\n        },\n        \"from-label\": {\n          type: \"Text\",\n          props: {\n            text: \"From\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"from-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Acme Corp\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"from-addr\": {\n          type: \"Text\",\n          props: {\n            text: \"123 Business Ave, Suite 100\\nSan Francisco, CA 94102\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.4,\n          },\n        },\n        \"from-email\": {\n          type: \"Text\",\n          props: {\n            text: \"billing@acmecorp.com\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"bill-to\": {\n          type: \"Column\",\n          props: {\n            gap: 4,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: 1,\n          },\n          children: [\"to-label\", \"to-name\", \"to-addr\", \"to-email\"],\n        },\n        \"to-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Bill To\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"to-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Globex Corporation\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"to-addr\": {\n          type: \"Text\",\n          props: {\n            text: \"456 Client Road\\nNew York, NY 10001\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.4,\n          },\n        },\n        \"to-email\": {\n          type: \"Text\",\n          props: {\n            text: \"accounts@globex.com\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-2\": { type: \"Spacer\", props: { height: 30 } },\n\n        // Items table\n        \"items-table\": {\n          type: \"Table\",\n          props: {\n            columns: [\n              { header: \"Description\", width: \"45%\", align: null },\n              { header: \"Qty\", width: \"10%\", align: \"center\" },\n              { header: \"Unit Price\", width: \"20%\", align: \"right\" },\n              { header: \"Amount\", width: \"25%\", align: \"right\" },\n            ],\n            rows: [\n              [\"Website Redesign\", \"1\", \"$5,000.00\", \"$5,000.00\"],\n              [\"SEO Optimization\", \"1\", \"$2,500.00\", \"$2,500.00\"],\n              [\"Content Writing (per page)\", \"12\", \"$150.00\", \"$1,800.00\"],\n              [\"Monthly Hosting\", \"6\", \"$50.00\", \"$300.00\"],\n            ],\n            headerBackgroundColor: \"#2d3748\",\n            headerTextColor: \"#ffffff\",\n            borderColor: \"#e2e8f0\",\n            fontSize: 10,\n            striped: true,\n          },\n        },\n\n        \"spacer-3\": { type: \"Spacer\", props: { height: 20 } },\n\n        // Totals\n        totals: {\n          type: \"Column\",\n          props: {\n            gap: 4,\n            alignItems: \"flex-end\",\n            justifyContent: null,\n            padding: null,\n            flex: null,\n          },\n          children: [\"subtotal\", \"tax\", \"divider-totals\", \"total\"],\n        },\n        subtotal: {\n          type: \"Text\",\n          props: {\n            text: \"Subtotal: $9,600.00\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        tax: {\n          type: \"Text\",\n          props: {\n            text: \"Tax (8.5%): $816.00\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"divider-totals\": {\n          type: \"Divider\",\n          props: {\n            color: \"#2d3748\",\n            thickness: 2,\n            marginTop: 4,\n            marginBottom: 4,\n          },\n        },\n        total: {\n          type: \"Text\",\n          props: {\n            text: \"Total Due: $10,416.00\",\n            fontSize: 14,\n            color: \"#1a202c\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-4\": { type: \"Spacer\", props: { height: 40 } },\n\n        footer: {\n          type: \"Text\",\n          props: {\n            text: \"Payment is due within 30 days. Please make checks payable to Acme Corp or wire transfer to the account details provided separately.\",\n            fontSize: 9,\n            color: \"#a0aec0\",\n            align: \"center\",\n            fontWeight: null,\n            fontStyle: \"italic\",\n            lineHeight: 1.4,\n          },\n        },\n        \"page-num\": {\n          type: \"PageNumber\",\n          props: {\n            format: null,\n            fontSize: 8,\n            color: \"#cbd5e0\",\n            align: \"center\",\n          },\n        },\n      },\n    },\n  },\n\n  // =========================================================================\n  // Report\n  // =========================================================================\n  {\n    name: \"report\",\n    label: \"Quarterly Report\",\n    description:\n      \"A business report with summary data, tables, and key findings\",\n    spec: {\n      root: \"doc\",\n      elements: {\n        doc: {\n          type: \"Document\",\n          props: {\n            title: \"Q4 2025 Report\",\n            author: \"Analytics Team\",\n            subject: \"Quarterly Performance\",\n          },\n          children: [\"page\"],\n        },\n        page: {\n          type: \"Page\",\n          props: {\n            size: \"A4\",\n            orientation: null,\n            marginTop: 50,\n            marginBottom: 60,\n            marginLeft: 50,\n            marginRight: 50,\n            backgroundColor: null,\n          },\n          children: [\n            \"title\",\n            \"subtitle\",\n            \"spacer-1\",\n            \"summary-heading\",\n            \"summary-text\",\n            \"spacer-2\",\n            \"metrics-table\",\n            \"spacer-3\",\n            \"findings-heading\",\n            \"findings-list\",\n            \"spacer-4\",\n            \"divider-1\",\n            \"recommendations-heading\",\n            \"recommendations-text\",\n            \"recommendations-list\",\n            \"page-num\",\n          ],\n        },\n\n        title: {\n          type: \"Heading\",\n          props: {\n            text: \"Q4 2025 Performance Report\",\n            level: \"h1\",\n            color: \"#1a202c\",\n            align: \"center\",\n          },\n        },\n        subtitle: {\n          type: \"Text\",\n          props: {\n            text: \"Analytics Division -- Prepared January 15, 2026\",\n            fontSize: 11,\n            color: \"#718096\",\n            align: \"center\",\n            fontWeight: null,\n            fontStyle: \"italic\",\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-1\": { type: \"Spacer\", props: { height: 30 } },\n\n        \"summary-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Executive Summary\",\n            level: \"h2\",\n            color: null,\n            align: null,\n          },\n        },\n        \"summary-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Q4 2025 demonstrated strong growth across all key performance indicators. Revenue increased 23% year-over-year, driven primarily by enterprise client acquisitions and expansion of existing accounts. Customer retention remained above target at 94.2%, while new customer acquisition exceeded projections by 15%.\",\n            fontSize: 11,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.6,\n          },\n        },\n\n        \"spacer-2\": { type: \"Spacer\", props: { height: 20 } },\n\n        \"metrics-table\": {\n          type: \"Table\",\n          props: {\n            columns: [\n              { header: \"Metric\", width: \"35%\", align: null },\n              { header: \"Q3 2025\", width: \"20%\", align: \"right\" },\n              { header: \"Q4 2025\", width: \"20%\", align: \"right\" },\n              { header: \"Change\", width: \"25%\", align: \"right\" },\n            ],\n            rows: [\n              [\"Revenue\", \"$4.2M\", \"$5.1M\", \"+21.4%\"],\n              [\"New Customers\", \"142\", \"168\", \"+18.3%\"],\n              [\"Customer Retention\", \"93.1%\", \"94.2%\", \"+1.1pp\"],\n              [\"Avg. Deal Size\", \"$28,400\", \"$32,100\", \"+13.0%\"],\n              [\"Support Tickets\", \"1,245\", \"1,089\", \"-12.5%\"],\n              [\"NPS Score\", \"72\", \"78\", \"+6 pts\"],\n            ],\n            headerBackgroundColor: \"#3b82f6\",\n            headerTextColor: \"#ffffff\",\n            borderColor: \"#e2e8f0\",\n            fontSize: 10,\n            striped: true,\n          },\n        },\n\n        \"spacer-3\": { type: \"Spacer\", props: { height: 20 } },\n\n        \"findings-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Key Findings\",\n            level: \"h2\",\n            color: null,\n            align: null,\n          },\n        },\n        \"findings-list\": {\n          type: \"List\",\n          props: {\n            items: [\n              \"Enterprise segment grew 34%, accounting for 62% of total revenue\",\n              \"Self-serve onboarding reduced time-to-value by 40%\",\n              \"APAC region showed strongest growth at 45% QoQ\",\n              \"Product usage increased 28% following the September feature release\",\n              \"Customer support resolution time decreased from 4.2hrs to 2.8hrs\",\n            ],\n            ordered: false,\n            fontSize: 10,\n            color: \"#4a5568\",\n            spacing: 6,\n          },\n        },\n\n        \"spacer-4\": { type: \"Spacer\", props: { height: 20 } },\n\n        \"divider-1\": {\n          type: \"Divider\",\n          props: {\n            color: \"#e2e8f0\",\n            thickness: 1,\n            marginTop: null,\n            marginBottom: null,\n          },\n        },\n\n        \"recommendations-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Recommendations\",\n            level: \"h2\",\n            color: null,\n            align: null,\n          },\n        },\n        \"recommendations-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Based on Q4 performance data, we recommend the following strategic priorities for Q1 2026:\",\n            fontSize: 11,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.5,\n          },\n        },\n        \"recommendations-list\": {\n          type: \"List\",\n          props: {\n            items: [\n              \"Increase APAC sales team headcount by 30% to capitalize on regional momentum\",\n              \"Invest in self-serve product analytics to further reduce onboarding friction\",\n              \"Launch enterprise-tier support SLA to improve retention in top accounts\",\n              \"Expand content marketing program targeting mid-market segment\",\n            ],\n            ordered: true,\n            fontSize: 10,\n            color: \"#4a5568\",\n            spacing: 6,\n          },\n        },\n\n        \"page-num\": {\n          type: \"PageNumber\",\n          props: {\n            format: \"Page {pageNumber} of {totalPages}\",\n            fontSize: 9,\n            color: \"#a0aec0\",\n            align: \"center\",\n          },\n        },\n      },\n    },\n  },\n\n  // =========================================================================\n  // Resume\n  // =========================================================================\n  {\n    name: \"resume\",\n    label: \"Resume\",\n    description:\n      \"A professional resume with contact info, experience, education, and skills\",\n    spec: {\n      root: \"doc\",\n      elements: {\n        doc: {\n          type: \"Document\",\n          props: {\n            title: \"Resume - Jane Cooper\",\n            author: \"Jane Cooper\",\n            subject: null,\n          },\n          children: [\"page\"],\n        },\n        page: {\n          type: \"Page\",\n          props: {\n            size: \"LETTER\",\n            orientation: null,\n            marginTop: 40,\n            marginBottom: 40,\n            marginLeft: 50,\n            marginRight: 50,\n            backgroundColor: null,\n          },\n          children: [\n            \"name\",\n            \"title-text\",\n            \"contact-row\",\n            \"divider-top\",\n            \"summary-heading\",\n            \"summary-text\",\n            \"divider-1\",\n            \"experience-heading\",\n            \"job-1-row\",\n            \"job-1-desc\",\n            \"job-1-highlights\",\n            \"spacer-1\",\n            \"job-2-row\",\n            \"job-2-desc\",\n            \"job-2-highlights\",\n            \"divider-2\",\n            \"education-heading\",\n            \"edu-row\",\n            \"divider-3\",\n            \"skills-heading\",\n            \"skills-row\",\n          ],\n        },\n\n        name: {\n          type: \"Heading\",\n          props: {\n            text: \"Jane Cooper\",\n            level: \"h1\",\n            color: \"#1a202c\",\n            align: \"center\",\n          },\n        },\n        \"title-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Senior Software Engineer\",\n            fontSize: 13,\n            color: \"#4a5568\",\n            align: \"center\",\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"contact-row\": {\n          type: \"Row\",\n          props: {\n            gap: 16,\n            alignItems: null,\n            justifyContent: \"center\",\n            padding: 6,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"contact-email\", \"contact-phone\", \"contact-site\"],\n        },\n        \"contact-email\": {\n          type: \"Link\",\n          props: {\n            text: \"jane@example.com\",\n            href: \"mailto:jane@example.com\",\n            fontSize: 10,\n            color: \"#3b82f6\",\n          },\n        },\n        \"contact-phone\": {\n          type: \"Text\",\n          props: {\n            text: \"(555) 123-4567\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"contact-site\": {\n          type: \"Link\",\n          props: {\n            text: \"janecooper.dev\",\n            href: \"https://janecooper.dev\",\n            fontSize: 10,\n            color: \"#3b82f6\",\n          },\n        },\n\n        \"divider-top\": {\n          type: \"Divider\",\n          props: {\n            color: \"#2d3748\",\n            thickness: 2,\n            marginTop: 8,\n            marginBottom: 8,\n          },\n        },\n\n        \"summary-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Summary\",\n            level: \"h3\",\n            color: \"#2d3748\",\n            align: null,\n          },\n        },\n        \"summary-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Software engineer with 8+ years building high-performance web applications. Expertise in TypeScript, React, and distributed systems. Passionate about developer experience, accessibility, and mentoring engineering teams.\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.5,\n          },\n        },\n\n        \"divider-1\": {\n          type: \"Divider\",\n          props: {\n            color: \"#e2e8f0\",\n            thickness: 1,\n            marginTop: 10,\n            marginBottom: 10,\n          },\n        },\n\n        \"experience-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Experience\",\n            level: \"h3\",\n            color: \"#2d3748\",\n            align: null,\n          },\n        },\n\n        \"job-1-row\": {\n          type: \"Row\",\n          props: {\n            gap: null,\n            alignItems: null,\n            justifyContent: \"space-between\",\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"job-1-title\", \"job-1-dates\"],\n        },\n        \"job-1-title\": {\n          type: \"Text\",\n          props: {\n            text: \"Senior Software Engineer -- Vercel\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"job-1-dates\": {\n          type: \"Text\",\n          props: {\n            text: \"2022 -- Present\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: \"italic\",\n            lineHeight: null,\n          },\n        },\n        \"job-1-desc\": {\n          type: \"Text\",\n          props: {\n            text: \"Led development of the Next.js deployment pipeline and developer tooling.\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"job-1-highlights\": {\n          type: \"List\",\n          props: {\n            items: [\n              \"Reduced build times by 45% through incremental compilation pipeline\",\n              \"Designed and shipped real-time collaboration features used by 50K+ teams\",\n              \"Mentored 4 junior engineers, 2 promoted to senior within 18 months\",\n            ],\n            ordered: false,\n            fontSize: 9,\n            color: \"#4a5568\",\n            spacing: 3,\n          },\n        },\n\n        \"spacer-1\": { type: \"Spacer\", props: { height: 10 } },\n\n        \"job-2-row\": {\n          type: \"Row\",\n          props: {\n            gap: null,\n            alignItems: null,\n            justifyContent: \"space-between\",\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"job-2-title\", \"job-2-dates\"],\n        },\n        \"job-2-title\": {\n          type: \"Text\",\n          props: {\n            text: \"Software Engineer -- Stripe\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"job-2-dates\": {\n          type: \"Text\",\n          props: {\n            text: \"2018 -- 2022\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: \"italic\",\n            lineHeight: null,\n          },\n        },\n        \"job-2-desc\": {\n          type: \"Text\",\n          props: {\n            text: \"Full-stack development on the Payments Dashboard and Connect platform.\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"job-2-highlights\": {\n          type: \"List\",\n          props: {\n            items: [\n              \"Built the Connect Express onboarding flow, reducing merchant setup time by 60%\",\n              \"Implemented real-time fraud detection UI processing 10M+ events/day\",\n              \"Led migration from legacy REST APIs to GraphQL, improving data loading by 3x\",\n            ],\n            ordered: false,\n            fontSize: 9,\n            color: \"#4a5568\",\n            spacing: 3,\n          },\n        },\n\n        \"divider-2\": {\n          type: \"Divider\",\n          props: {\n            color: \"#e2e8f0\",\n            thickness: 1,\n            marginTop: 10,\n            marginBottom: 10,\n          },\n        },\n\n        \"education-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"Education\",\n            level: \"h3\",\n            color: \"#2d3748\",\n            align: null,\n          },\n        },\n        \"edu-row\": {\n          type: \"Row\",\n          props: {\n            gap: null,\n            alignItems: null,\n            justifyContent: \"space-between\",\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"edu-degree\", \"edu-year\"],\n        },\n        \"edu-degree\": {\n          type: \"Text\",\n          props: {\n            text: \"B.S. Computer Science -- Stanford University\",\n            fontSize: 10,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"edu-year\": {\n          type: \"Text\",\n          props: {\n            text: \"2014 -- 2018\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: \"italic\",\n            lineHeight: null,\n          },\n        },\n\n        \"divider-3\": {\n          type: \"Divider\",\n          props: {\n            color: \"#e2e8f0\",\n            thickness: 1,\n            marginTop: 10,\n            marginBottom: 10,\n          },\n        },\n\n        \"skills-heading\": {\n          type: \"Heading\",\n          props: { text: \"Skills\", level: \"h3\", color: \"#2d3748\", align: null },\n        },\n        \"skills-row\": {\n          type: \"Row\",\n          props: {\n            gap: 20,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: null,\n            wrap: true,\n          },\n          children: [\"skills-langs\", \"skills-frameworks\", \"skills-infra\"],\n        },\n        \"skills-langs\": {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: 1,\n          },\n          children: [\"skills-langs-label\", \"skills-langs-list\"],\n        },\n        \"skills-langs-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Languages\",\n            fontSize: 9,\n            color: \"#718096\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"skills-langs-list\": {\n          type: \"Text\",\n          props: {\n            text: \"TypeScript, JavaScript, Python, Go, Rust, SQL\",\n            fontSize: 9,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.4,\n          },\n        },\n        \"skills-frameworks\": {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: 1,\n          },\n          children: [\"skills-fw-label\", \"skills-fw-list\"],\n        },\n        \"skills-fw-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Frameworks\",\n            fontSize: 9,\n            color: \"#718096\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"skills-fw-list\": {\n          type: \"Text\",\n          props: {\n            text: \"React, Next.js, Node.js, Express, GraphQL, Prisma\",\n            fontSize: 9,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.4,\n          },\n        },\n        \"skills-infra\": {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: 1,\n          },\n          children: [\"skills-infra-label\", \"skills-infra-list\"],\n        },\n        \"skills-infra-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Infrastructure\",\n            fontSize: 9,\n            color: \"#718096\",\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"skills-infra-list\": {\n          type: \"Text\",\n          props: {\n            text: \"AWS, Vercel, Docker, Kubernetes, Terraform, PostgreSQL\",\n            fontSize: 9,\n            color: \"#4a5568\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.4,\n          },\n        },\n      },\n    },\n  },\n\n  // =========================================================================\n  // Letter\n  // =========================================================================\n  {\n    name: \"letter\",\n    label: \"Business Letter\",\n    description:\n      \"A formal business letter with letterhead, body paragraphs, and signature\",\n    spec: {\n      root: \"doc\",\n      elements: {\n        doc: {\n          type: \"Document\",\n          props: {\n            title: \"Business Letter\",\n            author: \"Acme Corp\",\n            subject: null,\n          },\n          children: [\"page\"],\n        },\n        page: {\n          type: \"Page\",\n          props: {\n            size: \"LETTER\",\n            orientation: null,\n            marginTop: 60,\n            marginBottom: 60,\n            marginLeft: 70,\n            marginRight: 70,\n            backgroundColor: null,\n          },\n          children: [\n            \"letterhead\",\n            \"divider-head\",\n            \"spacer-1\",\n            \"date\",\n            \"spacer-2\",\n            \"recipient\",\n            \"spacer-3\",\n            \"greeting\",\n            \"spacer-4\",\n            \"body-1\",\n            \"spacer-5\",\n            \"body-2\",\n            \"spacer-6\",\n            \"body-3\",\n            \"spacer-7\",\n            \"closing\",\n            \"spacer-8\",\n            \"signature\",\n            \"sender-title\",\n            \"page-num\",\n          ],\n        },\n\n        letterhead: {\n          type: \"Row\",\n          props: {\n            gap: null,\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            padding: null,\n            flex: null,\n            wrap: null,\n          },\n          children: [\"lh-company\", \"lh-contact\"],\n        },\n        \"lh-company\": {\n          type: \"Heading\",\n          props: {\n            text: \"Acme Corp\",\n            level: \"h2\",\n            color: \"#2d3748\",\n            align: null,\n          },\n        },\n        \"lh-contact\": {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: \"flex-end\",\n            justifyContent: null,\n            padding: null,\n            flex: null,\n          },\n          children: [\"lh-addr\", \"lh-phone\", \"lh-web\"],\n        },\n        \"lh-addr\": {\n          type: \"Text\",\n          props: {\n            text: \"123 Business Ave, San Francisco, CA 94102\",\n            fontSize: 9,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"lh-phone\": {\n          type: \"Text\",\n          props: {\n            text: \"(555) 987-6543\",\n            fontSize: 9,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"lh-web\": {\n          type: \"Link\",\n          props: {\n            text: \"www.acmecorp.com\",\n            href: \"https://acmecorp.com\",\n            fontSize: 9,\n            color: \"#3b82f6\",\n          },\n        },\n\n        \"divider-head\": {\n          type: \"Divider\",\n          props: {\n            color: \"#2d3748\",\n            thickness: 2,\n            marginTop: 12,\n            marginBottom: null,\n          },\n        },\n\n        \"spacer-1\": { type: \"Spacer\", props: { height: 30 } },\n\n        date: {\n          type: \"Text\",\n          props: {\n            text: \"February 19, 2026\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-2\": { type: \"Spacer\", props: { height: 20 } },\n\n        recipient: {\n          type: \"Column\",\n          props: {\n            gap: 2,\n            alignItems: null,\n            justifyContent: null,\n            padding: null,\n            flex: null,\n          },\n          children: [\"rcpt-name\", \"rcpt-title\", \"rcpt-company\", \"rcpt-addr\"],\n        },\n        \"rcpt-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Mr. John Smith\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"rcpt-title\": {\n          type: \"Text\",\n          props: {\n            text: \"Director of Partnerships\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"rcpt-company\": {\n          type: \"Text\",\n          props: {\n            text: \"Globex Corporation\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"rcpt-addr\": {\n          type: \"Text\",\n          props: {\n            text: \"456 Client Road, New York, NY 10001\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-3\": { type: \"Spacer\", props: { height: 20 } },\n\n        greeting: {\n          type: \"Text\",\n          props: {\n            text: \"Dear Mr. Smith,\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-4\": { type: \"Spacer\", props: { height: 12 } },\n\n        \"body-1\": {\n          type: \"Text\",\n          props: {\n            text: \"Thank you for taking the time to meet with our team last Thursday. We greatly enjoyed the conversation about potential collaboration opportunities between Acme Corp and Globex Corporation, and we are excited about the prospect of working together.\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.6,\n          },\n        },\n\n        \"spacer-5\": { type: \"Spacer\", props: { height: 10 } },\n\n        \"body-2\": {\n          type: \"Text\",\n          props: {\n            text: \"As discussed, we believe our enterprise platform can significantly streamline your operations while reducing costs by an estimated 25-30%. Our team has prepared a detailed proposal outlining the implementation timeline, resource requirements, and projected ROI. I have enclosed this document for your review.\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.6,\n          },\n        },\n\n        \"spacer-6\": { type: \"Spacer\", props: { height: 10 } },\n\n        \"body-3\": {\n          type: \"Text\",\n          props: {\n            text: \"We would welcome the opportunity to schedule a follow-up meeting to address any questions you may have and discuss next steps. Please do not hesitate to reach out at your convenience. We look forward to the possibility of a fruitful partnership.\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: 1.6,\n          },\n        },\n\n        \"spacer-7\": { type: \"Spacer\", props: { height: 24 } },\n\n        closing: {\n          type: \"Text\",\n          props: {\n            text: \"Sincerely,\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"spacer-8\": { type: \"Spacer\", props: { height: 30 } },\n\n        signature: {\n          type: \"Text\",\n          props: {\n            text: \"Sarah Johnson\",\n            fontSize: 11,\n            color: null,\n            align: null,\n            fontWeight: \"bold\",\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n        \"sender-title\": {\n          type: \"Text\",\n          props: {\n            text: \"VP of Business Development, Acme Corp\",\n            fontSize: 10,\n            color: \"#718096\",\n            align: null,\n            fontWeight: null,\n            fontStyle: null,\n            lineHeight: null,\n          },\n        },\n\n        \"page-num\": {\n          type: \"PageNumber\",\n          props: {\n            format: null,\n            fontSize: 8,\n            color: \"#cbd5e0\",\n            align: \"center\",\n          },\n        },\n      },\n    },\n  },\n];\n"
  },
  {
    "path": "examples/react-pdf/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:react-pdf:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:react-pdf:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/react-pdf/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/react-pdf/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/react-pdf/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  serverExternalPackages: [\"@react-pdf/renderer\", \"@json-render/react-pdf\"],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/react-pdf/package.json",
    "content": "{\n  \"name\": \"example-react-pdf\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless react-pdf-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.52\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react-pdf\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"ai\": \"6.0.94\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.575.0\",\n    \"next\": \"16.1.6\",\n    \"radix-ui\": \"^1.4.3\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"react-resizable-panels\": \"^4.4.1\",\n    \"tailwind-merge\": \"^3.5.0\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"shadcn\": \"^3.8.5\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/react-pdf/postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/react-pdf/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/react-three-fiber/CHANGELOG.md",
    "content": "# example-react-three-fiber\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/react-three-fiber@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n  - @json-render/react-three-fiber@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react-three-fiber@0.13.0\n  - @json-render/react@0.13.0\n"
  },
  {
    "path": "examples/react-three-fiber/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\n\nexport const metadata: Metadata = {\n  title: \"json-render React Three Fiber Example\",\n  description: \"3D scenes from JSON specs with @json-render/react-three-fiber\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body style={{ margin: 0, fontFamily: \"system-ui, sans-serif\" }}>\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/react-three-fiber/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useSyncExternalStore, useCallback } from \"react\";\nimport { defineCatalog } from \"@json-render/core\";\nimport type { ComputedFunction } from \"@json-render/core\";\nimport { schema, defineRegistry } from \"@json-render/react\";\nimport {\n  threeComponentDefinitions,\n  threeComponents,\n  ThreeCanvas,\n} from \"@json-render/react-three-fiber\";\nimport { scenes } from \"./scenes\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...threeComponentDefinitions,\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    ...threeComponents,\n  },\n});\n\nfunction num(v: unknown, fallback: number): number {\n  return typeof v === \"number\" ? v : fallback;\n}\n\nconst computedFunctions: Record<string, ComputedFunction> = {\n  halfHeight: (a) => num(a.h, 1) / 2,\n\n  helixX: (a) => {\n    const angle =\n      (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +\n      num(a.strand, 0) * Math.PI;\n    return num(a.radius, 1.8) * Math.cos(angle);\n  },\n  helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6),\n  helixZ: (a) => {\n    const angle =\n      (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +\n      num(a.strand, 0) * Math.PI;\n    return num(a.radius, 1.8) * Math.sin(angle);\n  },\n  helixHue: (a) => {\n    const t = num(a.i, 0) / num(a.count, 40);\n    return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`;\n  },\n\n  spiralX: (a) => {\n    const t = num(a.i, 0) / num(a.count, 60);\n    const r = 0.5 + t * num(a.maxRadius, 7);\n    const angle = t * num(a.turns, 4) * 2 * Math.PI;\n    return r * Math.cos(angle);\n  },\n  spiralZ: (a) => {\n    const t = num(a.i, 0) / num(a.count, 60);\n    const r = 0.5 + t * num(a.maxRadius, 7);\n    const angle = t * num(a.turns, 4) * 2 * Math.PI;\n    return r * Math.sin(angle);\n  },\n  spiralY: (a) => {\n    const i = num(a.i, 0);\n    return Math.sin(i * 1.7) * 0.4;\n  },\n  spiralScale: (a) => {\n    const t = num(a.i, 0) / num(a.count, 60);\n    return 0.08 + (1 - t) * 0.25;\n  },\n  spiralEmissive: (a) => {\n    const t = num(a.i, 0) / num(a.count, 60);\n    return Math.max(0, 1 - t * 1.5);\n  },\n\n  circleX: (a) => {\n    const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;\n    return num(a.radius, 3) * Math.cos(angle);\n  },\n  circleZ: (a) => {\n    const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;\n    return num(a.radius, 3) * Math.sin(angle);\n  },\n  circleY: (a) => {\n    const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;\n    return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3);\n  },\n  circleAngle: (a) => {\n    return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;\n  },\n  circleHue: (a) => {\n    const t = num(a.i, 0) / num(a.count, 8);\n    return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`;\n  },\n};\n\nfunction highlightJson(json: string): string {\n  return json.replace(\n    /(\"(?:\\\\.|[^\"\\\\])*\")\\s*(:)|(\"(?:\\\\.|[^\"\\\\])*\")|([-+]?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)|(\\btrue\\b|\\bfalse\\b|\\bnull\\b)|([{}[\\]:,])/g,\n    (_, key, colon, str, num, lit, punct) => {\n      if (key) return `<span style=\"color:#FF4D8D\">${key}</span>${colon}`;\n      if (str) return `<span style=\"color:#00CA50\">${str}</span>`;\n      if (num) return `<span style=\"color:#47A8FF\">${num}</span>`;\n      if (lit) return `<span style=\"color:#47A8FF\">${lit}</span>`;\n      if (punct) return `<span style=\"color:#666\">${punct}</span>`;\n      return _;\n    },\n  );\n}\n\nconst MOBILE_BREAKPOINT = 768;\n\nfunction subscribeToResize(cb: () => void) {\n  window.addEventListener(\"resize\", cb);\n  return () => window.removeEventListener(\"resize\", cb);\n}\n\nfunction getIsMobile() {\n  return typeof window !== \"undefined\"\n    ? window.innerWidth < MOBILE_BREAKPOINT\n    : false;\n}\n\nfunction useIsMobile() {\n  return useSyncExternalStore(subscribeToResize, getIsMobile, () => false);\n}\n\nconst LIST_WIDTH = 220;\nconst JSON_WIDTH = 380;\nconst HEADER_HEIGHT = 40;\nconst MOBILE_HEADER_HEIGHT = 48;\n\nconst headerStyle: React.CSSProperties = {\n  height: HEADER_HEIGHT,\n  display: \"flex\",\n  alignItems: \"center\",\n  padding: \"0 16px\",\n  fontSize: 11,\n  fontWeight: 600,\n  color: \"#555\",\n  letterSpacing: \"0.08em\",\n  textTransform: \"uppercase\",\n  fontFamily: \"ui-monospace, monospace\",\n  borderBottom: \"1px solid #1e1e1e\",\n  flexShrink: 0,\n  whiteSpace: \"nowrap\",\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n  boxSizing: \"border-box\",\n};\n\nfunction MobileLayout() {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [showJson, setShowJson] = useState(false);\n  const [showScenes, setShowScenes] = useState(false);\n  const selected = scenes[selectedIndex]!;\n\n  const closePanels = useCallback(() => {\n    setShowJson(false);\n    setShowScenes(false);\n  }, []);\n\n  return (\n    <div\n      style={{\n        height: \"100dvh\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        background: \"#0a0a0a\",\n        overflow: \"hidden\",\n      }}\n    >\n      <div\n        style={{\n          height: MOBILE_HEADER_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          padding: \"0 12px\",\n          borderBottom: \"1px solid #1e1e1e\",\n          background: \"#0f0f0f\",\n          flexShrink: 0,\n          gap: 8,\n        }}\n      >\n        <button\n          onClick={() => {\n            setShowScenes((v) => !v);\n            setShowJson(false);\n          }}\n          style={{\n            background: showScenes ? \"rgba(255,255,255,0.1)\" : \"transparent\",\n            border: \"1px solid #333\",\n            borderRadius: 6,\n            color: \"#ccc\",\n            fontSize: 12,\n            fontWeight: 500,\n            padding: \"6px 10px\",\n            cursor: \"pointer\",\n            fontFamily: \"inherit\",\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n            maxWidth: \"50%\",\n          }}\n        >\n          {selected.name}\n        </button>\n\n        <span\n          style={{\n            flex: 1,\n            fontSize: 10,\n            color: \"#555\",\n            textAlign: \"center\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n            whiteSpace: \"nowrap\",\n            fontFamily: \"ui-monospace, monospace\",\n            textTransform: \"uppercase\",\n            letterSpacing: \"0.06em\",\n          }}\n        >\n          {selected.description}\n        </span>\n\n        <button\n          onClick={() => {\n            setShowJson((v) => !v);\n            setShowScenes(false);\n          }}\n          style={{\n            background: showJson ? \"rgba(255,255,255,0.1)\" : \"transparent\",\n            border: \"1px solid #333\",\n            borderRadius: 6,\n            color: \"#ccc\",\n            fontSize: 11,\n            fontWeight: 500,\n            padding: \"6px 10px\",\n            cursor: \"pointer\",\n            fontFamily: \"ui-monospace, monospace\",\n            whiteSpace: \"nowrap\",\n            letterSpacing: \"0.04em\",\n          }}\n        >\n          JSON\n        </button>\n      </div>\n\n      <div style={{ flex: 1, position: \"relative\", minHeight: 0 }}>\n        <ThreeCanvas\n          key={selectedIndex}\n          spec={selected.spec}\n          registry={registry}\n          functions={computedFunctions}\n          shadows\n          camera={{ position: [0, 0, 5], fov: 50 }}\n          style={{ width: \"100%\", height: \"100%\" }}\n        />\n\n        {showScenes && (\n          <>\n            <div\n              onClick={closePanels}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                background: \"rgba(0,0,0,0.5)\",\n                zIndex: 10,\n              }}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                top: 0,\n                left: 0,\n                bottom: 0,\n                width: \"75%\",\n                maxWidth: 280,\n                background: \"#0f0f0f\",\n                borderRight: \"1px solid #1e1e1e\",\n                zIndex: 11,\n                display: \"flex\",\n                flexDirection: \"column\",\n                overflowY: \"auto\",\n                WebkitOverflowScrolling: \"touch\",\n              }}\n            >\n              <div style={{ ...headerStyle, height: 36 }}>Scenes</div>\n              <div style={{ padding: \"6px 0\" }}>\n                {scenes.map((scene, i) => (\n                  <button\n                    key={scene.name}\n                    onClick={() => {\n                      setSelectedIndex(i);\n                      setShowScenes(false);\n                    }}\n                    style={{\n                      display: \"block\",\n                      width: \"100%\",\n                      padding: \"10px 16px\",\n                      fontSize: 14,\n                      border: \"none\",\n                      textAlign: \"left\",\n                      background:\n                        i === selectedIndex\n                          ? \"rgba(255,255,255,0.08)\"\n                          : \"transparent\",\n                      color: i === selectedIndex ? \"#fff\" : \"#888\",\n                      fontWeight: i === selectedIndex ? 500 : 400,\n                      cursor: \"pointer\",\n                      borderLeft:\n                        i === selectedIndex\n                          ? \"2px solid #fff\"\n                          : \"2px solid transparent\",\n                      fontFamily: \"inherit\",\n                    }}\n                  >\n                    {scene.name}\n                  </button>\n                ))}\n              </div>\n            </div>\n          </>\n        )}\n\n        {showJson && (\n          <>\n            <div\n              onClick={closePanels}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                background: \"rgba(0,0,0,0.5)\",\n                zIndex: 10,\n              }}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                top: 0,\n                right: 0,\n                bottom: 0,\n                width: \"85%\",\n                maxWidth: 400,\n                background: \"#0d0d0d\",\n                borderLeft: \"1px solid #1e1e1e\",\n                zIndex: 11,\n                display: \"flex\",\n                flexDirection: \"column\",\n              }}\n            >\n              <div style={{ ...headerStyle, height: 36 }}>Spec JSON</div>\n              <pre\n                style={{\n                  flex: 1,\n                  margin: 0,\n                  padding: 14,\n                  overflowY: \"auto\",\n                  overflowX: \"auto\",\n                  WebkitOverflowScrolling: \"touch\",\n                  fontSize: 11,\n                  lineHeight: 1.6,\n                  fontFamily:\n                    \"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace\",\n                  color: \"#EDEDED\",\n                  tabSize: 2,\n                }}\n                dangerouslySetInnerHTML={{\n                  __html: highlightJson(JSON.stringify(selected.spec, null, 2)),\n                }}\n              />\n            </div>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction DesktopLayout() {\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const selected = scenes[selectedIndex]!;\n\n  return (\n    <div style={{ height: \"100vh\", display: \"flex\", background: \"#0a0a0a\" }}>\n      <div\n        style={{\n          width: LIST_WIDTH,\n          flexShrink: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          borderRight: \"1px solid #1e1e1e\",\n          background: \"#0f0f0f\",\n        }}\n      >\n        <div style={headerStyle}>Scenes</div>\n        <div style={{ flex: 1, overflowY: \"auto\", padding: \"6px 0\" }}>\n          {scenes.map((scene, i) => (\n            <button\n              key={scene.name}\n              onClick={() => setSelectedIndex(i)}\n              style={{\n                display: \"block\",\n                width: \"100%\",\n                padding: \"8px 16px\",\n                fontSize: 13,\n                border: \"none\",\n                textAlign: \"left\",\n                background:\n                  i === selectedIndex\n                    ? \"rgba(255,255,255,0.08)\"\n                    : \"transparent\",\n                color: i === selectedIndex ? \"#fff\" : \"#888\",\n                fontWeight: i === selectedIndex ? 500 : 400,\n                cursor: \"pointer\",\n                borderLeft:\n                  i === selectedIndex\n                    ? \"2px solid #fff\"\n                    : \"2px solid transparent\",\n                fontFamily: \"inherit\",\n              }}\n            >\n              {scene.name}\n            </button>\n          ))}\n        </div>\n      </div>\n\n      <div\n        style={{\n          flex: 1,\n          display: \"flex\",\n          flexDirection: \"column\",\n          minWidth: 0,\n        }}\n      >\n        <div style={headerStyle}>{selected.description}</div>\n        <div style={{ flex: 1, background: \"#000\", position: \"relative\" }}>\n          <ThreeCanvas\n            key={selectedIndex}\n            spec={selected.spec}\n            registry={registry}\n            functions={computedFunctions}\n            shadows\n            camera={{ position: [0, 0, 5], fov: 50 }}\n            style={{ width: \"100%\", height: \"100%\" }}\n          />\n        </div>\n      </div>\n\n      <div\n        style={{\n          width: JSON_WIDTH,\n          flexShrink: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          borderLeft: \"1px solid #1e1e1e\",\n          background: \"#0d0d0d\",\n        }}\n      >\n        <div style={headerStyle}>Spec JSON</div>\n        <pre\n          style={{\n            flex: 1,\n            margin: 0,\n            padding: 14,\n            overflowY: \"auto\",\n            overflowX: \"auto\",\n            fontSize: 11,\n            lineHeight: 1.6,\n            fontFamily:\n              \"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace\",\n            color: \"#EDEDED\",\n            tabSize: 2,\n          }}\n          dangerouslySetInnerHTML={{\n            __html: highlightJson(JSON.stringify(selected.spec, null, 2)),\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default function Page() {\n  const isMobile = useIsMobile();\n  return isMobile ? <MobileLayout /> : <DesktopLayout />;\n}\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/_helpers.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport type Scene = { name: string; description: string; spec: Spec };\n\nexport const PI = Math.PI;\n\nexport function range(n: number) {\n  return Array.from({ length: n }, (_, i) => ({ i }));\n}\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/clockwork-orrery.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const clockworkOrrery: Scene = {\n  name: \"Clockwork Orrery\",\n  description:\n    \"Interlocking brass/gold rings spinning at different tilts and speeds with orbiting metallic planets. Like the inside of a celestial clockwork mechanism.\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"env\",\n          \"ambient\",\n          \"key\",\n          \"warm\",\n          \"accent\",\n          \"center\",\n          \"ring-spin-0\",\n          \"ring-spin-1\",\n          \"ring-spin-2\",\n          \"ring-spin-3\",\n          \"planet-orbit-0\",\n          \"planet-orbit-1\",\n          \"planet-orbit-2\",\n          \"sparkles\",\n          \"post\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 3, 8],\n          fov: 50,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"dawn\",\n          background: true,\n          blur: 1,\n          intensity: 0.5,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#332200\", intensity: 0.4 },\n        children: [],\n      },\n      key: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [5, 10, 5],\n          rotation: null,\n          scale: null,\n          color: \"#ffddaa\",\n          intensity: 2,\n          castShadow: true,\n        },\n        children: [],\n      },\n      warm: {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#ffaa44\",\n          intensity: 30,\n          distance: 15,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      accent: {\n        type: \"PointLight\",\n        props: {\n          position: [-4, 3, -4],\n          rotation: null,\n          scale: null,\n          color: \"#ffcc88\",\n          intensity: 15,\n          distance: 15,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      center: {\n        type: \"Float\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.5,\n          rotationIntensity: 0.1,\n          floatIntensity: 0.2,\n          enabled: true,\n        },\n        children: [\"center-sphere\"],\n      },\n      \"center-sphere\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.5,\n          widthSegments: 32,\n          heightSegments: 16,\n          color: \"#ffddaa\",\n          transmission: 1,\n          thickness: 0.8,\n          roughness: 0,\n          chromaticAberration: 0.1,\n          ior: 1.8,\n          distortion: 0.05,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 10,\n          resolution: 256,\n        },\n        children: [],\n      },\n      \"ring-spin-0\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [0.3, 0, 0.8],\n          scale: null,\n          speed: 1.2,\n          axis: \"y\",\n        },\n        children: [\"ring-torus-0\"],\n      },\n      \"ring-torus-0\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ffcc44\",\n            metalness: 0.95,\n            roughness: 0.1,\n            emissive: \"#cc9900\",\n            emissiveIntensity: 0.4,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 2,\n          tube: 0.04,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"ring-spin-1\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [0.9, 0, -0.2],\n          scale: null,\n          speed: -0.7,\n          axis: \"y\",\n        },\n        children: [\"ring-torus-1\"],\n      },\n      \"ring-torus-1\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#cc8833\",\n            metalness: 0.95,\n            roughness: 0.1,\n            emissive: \"#aa6622\",\n            emissiveIntensity: 0.3,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 3.2,\n          tube: 0.035,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"ring-spin-2\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [-0.4, 0, 0.5],\n          scale: null,\n          speed: 0.4,\n          axis: \"y\",\n        },\n        children: [\"ring-torus-2\"],\n      },\n      \"ring-torus-2\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#bb9944\",\n            metalness: 0.95,\n            roughness: 0.1,\n            emissive: \"#886622\",\n            emissiveIntensity: 0.25,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 4.5,\n          tube: 0.03,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"ring-spin-3\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [0.1, 0, -0.1],\n          scale: null,\n          speed: -0.2,\n          axis: \"y\",\n        },\n        children: [\"ring-torus-3\"],\n      },\n      \"ring-torus-3\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#997744\",\n            metalness: 0.95,\n            roughness: 0.1,\n            emissive: \"#664422\",\n            emissiveIntensity: 0.15,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 6,\n          tube: 0.025,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"planet-orbit-0\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.6,\n          radius: 2.5,\n          tilt: 0.3,\n        },\n        children: [\"planet-spin-0\"],\n      },\n      \"planet-spin-0\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 3,\n          axis: \"y\",\n        },\n        children: [\"planet-sphere-0\"],\n      },\n      \"planet-sphere-0\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ffffff\",\n            metalness: 1,\n            roughness: 0.02,\n            emissive: \"#ffffff\",\n            emissiveIntensity: 0.2,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.2,\n          widthSegments: 32,\n          heightSegments: 16,\n        },\n        children: [],\n      },\n      \"planet-orbit-1\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: -0.9,\n          radius: 3.8,\n          tilt: 0.8,\n        },\n        children: [\"planet-spin-1\"],\n      },\n      \"planet-spin-1\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -2,\n          axis: \"x\",\n        },\n        children: [\"planet-sphere-1\"],\n      },\n      \"planet-sphere-1\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ffcc88\",\n            metalness: 0.8,\n            roughness: 0.15,\n            emissive: \"#ffcc88\",\n            emissiveIntensity: 0.2,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.15,\n          widthSegments: 32,\n          heightSegments: 16,\n        },\n        children: [],\n      },\n      \"planet-orbit-2\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.35,\n          radius: 5.2,\n          tilt: -0.5,\n        },\n        children: [\"planet-spin-2\"],\n      },\n      \"planet-spin-2\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 1.5,\n          axis: \"z\",\n        },\n        children: [\"planet-sphere-2\"],\n      },\n      \"planet-sphere-2\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ccaa66\",\n            metalness: 0.9,\n            roughness: 0.05,\n            emissive: \"#ccaa66\",\n            emissiveIntensity: 0.2,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.18,\n          widthSegments: 32,\n          heightSegments: 16,\n        },\n        children: [],\n      },\n      sparkles: {\n        type: \"Sparkles\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: [14, 10, 14],\n          count: 150,\n          speed: 0.15,\n          opacity: 0.3,\n          color: \"#ffcc66\",\n          size: 1,\n          noise: 2,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 1.2,\n          luminanceThreshold: 0.2,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.5, darkness: 0.5 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 4,\n          maxDistance: 18,\n          minPolarAngle: 0.3,\n          maxPolarAngle: 1.5,\n          autoRotate: true,\n          autoRotateSpeed: 0.3,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/deep-sea-abyss.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const deepSeaAbyss: Scene = {\n  name: \"Deep Sea Abyss\",\n  description:\n    \"Bioluminescent deep ocean. Pitch black with glowing jellyfish-like creatures.\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"env\",\n          \"fog\",\n          \"ambient\",\n          \"jelly-1-pulse\",\n          \"jelly-2-pulse\",\n          \"jelly-3-pulse\",\n          \"light-1\",\n          \"light-2\",\n          \"light-3\",\n          \"tentacles-1\",\n          \"tentacles-2\",\n          \"tentacles-3\",\n          \"sparkles-1\",\n          \"sparkles-2\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 1, 6],\n          fov: 55,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"night\",\n          background: false,\n          blur: 1,\n          intensity: 0.05,\n        },\n        children: [],\n      },\n      fog: {\n        type: \"Fog\",\n        props: { color: \"#000510\", near: 2, far: 18 },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#001133\", intensity: 0.15 },\n        children: [],\n      },\n      \"jelly-1-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [0, 1, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.6,\n          min: 0.85,\n          max: 1.15,\n        },\n        children: [\"jelly-1-spin\"],\n      },\n      \"jelly-1-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.2,\n          axis: \"y\",\n        },\n        children: [\"jelly-1-body\"],\n      },\n      \"jelly-1-body\": {\n        type: \"DistortSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 1,\n          widthSegments: 64,\n          heightSegments: 32,\n          color: \"#00ccff\",\n          speed: 2,\n          distort: 0.5,\n          metalness: 0,\n          roughness: 0.2,\n        },\n        children: [],\n      },\n      \"jelly-2-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [-4, -1, -3],\n          rotation: null,\n          scale: null,\n          speed: 0.8,\n          min: 0.8,\n          max: 1.2,\n        },\n        children: [\"jelly-2-spin\"],\n      },\n      \"jelly-2-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.3,\n          axis: \"y\",\n        },\n        children: [\"jelly-2-body\"],\n      },\n      \"jelly-2-body\": {\n        type: \"DistortSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.7,\n          widthSegments: 64,\n          heightSegments: 32,\n          color: \"#cc44ff\",\n          speed: 2.5,\n          distort: 0.6,\n          metalness: 0,\n          roughness: 0.2,\n        },\n        children: [],\n      },\n      \"jelly-3-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [3, 0.5, -5],\n          rotation: null,\n          scale: null,\n          speed: 0.5,\n          min: 0.9,\n          max: 1.1,\n        },\n        children: [\"jelly-3-spin\"],\n      },\n      \"jelly-3-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.15,\n          axis: \"y\",\n        },\n        children: [\"jelly-3-body\"],\n      },\n      \"jelly-3-body\": {\n        type: \"DistortSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.5,\n          widthSegments: 64,\n          heightSegments: 32,\n          color: \"#ff44aa\",\n          speed: 1.5,\n          distort: 0.4,\n          metalness: 0,\n          roughness: 0.3,\n        },\n        children: [],\n      },\n      \"light-1\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#00ccff\",\n          intensity: 20,\n          distance: 12,\n          decay: 2,\n          castShadow: null,\n        },\n        children: [],\n      },\n      \"light-2\": {\n        type: \"PointLight\",\n        props: {\n          position: [-4, -1, -3],\n          rotation: null,\n          scale: null,\n          color: \"#cc44ff\",\n          intensity: 15,\n          distance: 10,\n          decay: 2,\n          castShadow: null,\n        },\n        children: [],\n      },\n      \"light-3\": {\n        type: \"PointLight\",\n        props: {\n          position: [3, 0.5, -5],\n          rotation: null,\n          scale: null,\n          color: \"#ff44aa\",\n          intensity: 10,\n          distance: 8,\n          decay: 2,\n          castShadow: null,\n        },\n        children: [],\n      },\n      \"tentacles-1\": {\n        type: \"Float\",\n        props: {\n          position: [0, -1, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.3,\n          rotationIntensity: 0.5,\n          floatIntensity: 0.8,\n          enabled: true,\n        },\n        children: [\"t1-s1\", \"t1-s2\", \"t1-s3\", \"t1-s4\", \"t1-s5\", \"t1-s6\"],\n      },\n      \"t1-s1\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.3, -0.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.12,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t1-s2\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.2, -1.2, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.1,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t1-s3\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.4, -1.9, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.08,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t1-s4\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.3, -2.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.07,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t1-s5\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.1, -3.0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.06,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t1-s6\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.1, -3.4, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.05,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#00ccff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"tentacles-2\": {\n        type: \"Float\",\n        props: {\n          position: [-4, -2.5, -3],\n          rotation: null,\n          scale: null,\n          speed: 0.4,\n          rotationIntensity: 0.5,\n          floatIntensity: 0.8,\n          enabled: true,\n        },\n        children: [\"t2-s1\", \"t2-s2\", \"t2-s3\", \"t2-s4\", \"t2-s5\"],\n      },\n      \"t2-s1\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.3, -0.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.1,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#cc44ff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t2-s2\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.2, -1.1, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.08,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#cc44ff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t2-s3\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.4, -1.7, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.06,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#cc44ff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t2-s4\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.3, -2.2, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.05,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#cc44ff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t2-s5\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.1, -2.6, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.05,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#cc44ff\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"tentacles-3\": {\n        type: \"Float\",\n        props: {\n          position: [3, -1, -5],\n          rotation: null,\n          scale: null,\n          speed: 0.35,\n          rotationIntensity: 0.5,\n          floatIntensity: 0.8,\n          enabled: true,\n        },\n        children: [\"t3-s1\", \"t3-s2\", \"t3-s3\", \"t3-s4\"],\n      },\n      \"t3-s1\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.3, -0.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.09,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#ff44aa\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t3-s2\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.2, -1.0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.07,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#ff44aa\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t3-s3\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [0.4, -1.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.06,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#ff44aa\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"t3-s4\": {\n        type: \"GlassSphere\",\n        props: {\n          position: [-0.1, -1.9, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.05,\n          widthSegments: 16,\n          heightSegments: 8,\n          color: \"#ff44aa\",\n          transmission: 0.9,\n          thickness: 0.3,\n          roughness: 0.1,\n          chromaticAberration: 0.1,\n          ior: 1.5,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      \"sparkles-1\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -3],\n          rotation: null,\n          scale: [15, 10, 15],\n          count: 300,\n          speed: 0.1,\n          opacity: 0.4,\n          color: \"#00aaff\",\n          size: 1,\n          noise: 3,\n        },\n        children: [],\n      },\n      \"sparkles-2\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, -2, 0],\n          rotation: null,\n          scale: [12, 6, 12],\n          count: 150,\n          speed: 0.05,\n          opacity: 0.2,\n          color: \"#8844ff\",\n          size: 0.8,\n          noise: 2,\n        },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 3,\n          maxDistance: 18,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: true,\n          autoRotateSpeed: 0.15,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/floating-islands.ts",
    "content": "import { PI } from \"./_helpers\";\nimport type { Scene } from \"./_helpers\";\n\nexport const floatingIslands: Scene = {\n  name: \"Floating Islands\",\n  description:\n    \"Sky, clouds, islands with trees and bridges floating in sunset light\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"sky\",\n          \"ambient\",\n          \"sun\",\n          \"clouds\",\n          \"island-main-float\",\n          \"island-left-float\",\n          \"island-right-float\",\n          \"island-far-float\",\n          \"island-tiny1-float\",\n          \"island-tiny2-float\",\n          \"bridge-1\",\n          \"bridge-2\",\n          \"sparkles\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [8, 6, 14],\n          rotation: null,\n          scale: null,\n          fov: 50,\n          near: 0.1,\n          far: 500,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      sky: {\n        type: \"Environment\",\n        props: {\n          preset: \"dawn\",\n          background: true,\n          blur: 0.8,\n          intensity: 0.5,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#ffd4a0\", intensity: 0.5 },\n        children: [],\n      },\n      sun: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [8, 12, 6],\n          rotation: null,\n          scale: null,\n          color: \"#ffcc88\",\n          intensity: 2.5,\n          castShadow: true,\n        },\n        children: [],\n      },\n      clouds: {\n        type: \"Cloud\",\n        props: {\n          position: [0, 4, -8],\n          rotation: null,\n          scale: null,\n          seed: 7,\n          segments: 40,\n          bounds: [25, 3, 20],\n          volume: 10,\n          speed: 0.1,\n          fade: 25,\n          opacity: 0.6,\n          color: \"#ffe8d0\",\n          growth: 8,\n        },\n        children: [],\n      },\n\n      \"island-main-float\": {\n        type: \"Float\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.4,\n          rotationIntensity: 0.02,\n          floatIntensity: 0.3,\n          enabled: true,\n        },\n        children: [\"island-main\"],\n      },\n      \"island-main\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"main-base\",\n          \"main-top\",\n          \"main-underhang\",\n          \"tree-1-trunk\",\n          \"tree-1-canopy\",\n          \"tree-2-trunk\",\n          \"tree-2-canopy\",\n          \"tree-3-trunk\",\n          \"tree-3-canopy\",\n          \"house-base\",\n          \"house-roof\",\n        ],\n      },\n      \"main-base\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, -0.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 3,\n          radiusBottom: 1.5,\n          height: 2,\n          radialSegments: 16,\n        },\n        children: [],\n      },\n      \"main-top\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.55, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#4a8c3f\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 3.1,\n          radiusBottom: 3,\n          height: 0.3,\n          radialSegments: 16,\n        },\n        children: [],\n      },\n      \"main-underhang\": {\n        type: \"Cone\",\n        props: {\n          position: [0, -2.5, 0],\n          rotation: [PI, 0, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#6B4513\",\n            metalness: 0.1,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 1.5,\n          height: 2.5,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"tree-1-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, 1, 0.5],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B5A2B\",\n            metalness: 0,\n            roughness: 1,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 0.06,\n          radiusBottom: 0.1,\n          height: 1.5,\n          radialSegments: 8,\n        },\n        children: [],\n      },\n      \"tree-1-canopy\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 2.2, 0.5],\n          rotation: null,\n          scale: [1, 1.2, 1],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#2d7a2d\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.6,\n          widthSegments: 16,\n          heightSegments: 12,\n        },\n        children: [],\n      },\n      \"tree-2-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-1.2, 0.8, -0.8],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B5A2B\",\n            metalness: 0,\n            roughness: 1,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 0.05,\n          radiusBottom: 0.08,\n          height: 1.1,\n          radialSegments: 8,\n        },\n        children: [],\n      },\n      \"tree-2-canopy\": {\n        type: \"Sphere\",\n        props: {\n          position: [-1.2, 1.7, -0.8],\n          rotation: null,\n          scale: [1, 1.3, 1],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#3a8c3a\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.45,\n          widthSegments: 16,\n          heightSegments: 12,\n        },\n        children: [],\n      },\n      \"tree-3-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-0.3, 1.2, 1.5],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B5A2B\",\n            metalness: 0,\n            roughness: 1,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 0.07,\n          radiusBottom: 0.12,\n          height: 2,\n          radialSegments: 8,\n        },\n        children: [],\n      },\n      \"tree-3-canopy\": {\n        type: \"Sphere\",\n        props: {\n          position: [-0.3, 2.7, 1.5],\n          rotation: null,\n          scale: [1.2, 1.4, 1.2],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#1a6b1a\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.7,\n          widthSegments: 16,\n          heightSegments: 12,\n        },\n        children: [],\n      },\n      \"house-base\": {\n        type: \"RoundedBox\",\n        props: {\n          position: [0, 0.9, -0.5],\n          rotation: [0, 0.3, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#e8d8c0\",\n            metalness: 0,\n            roughness: 0.8,\n            emissive: \"#ffddaa\",\n            emissiveIntensity: 0.05,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          width: 0.8,\n          height: 0.7,\n          depth: 0.7,\n          radius: 0.05,\n          smoothness: 4,\n        },\n        children: [],\n      },\n      \"house-roof\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 1.55, -0.5],\n          rotation: [0, 1.085, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#a04030\",\n            metalness: 0.1,\n            roughness: 0.8,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.6,\n          height: 0.5,\n          radialSegments: 4,\n        },\n        children: [],\n      },\n\n      \"island-left-float\": {\n        type: \"Float\",\n        props: {\n          position: [-6, 0.1, 2],\n          rotation: null,\n          scale: null,\n          speed: 0.6,\n          rotationIntensity: 0.03,\n          floatIntensity: 0.5,\n          enabled: true,\n        },\n        children: [\"island-left\"],\n      },\n      \"island-left\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"left-base\",\n          \"left-top\",\n          \"left-under\",\n          \"left-tree-t\",\n          \"left-tree-c\",\n        ],\n      },\n      \"left-base\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, -0.3, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 1.8,\n          radiusBottom: 0.8,\n          height: 1.2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"left-top\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.25, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#4a8c3f\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 1.85,\n          radiusBottom: 1.8,\n          height: 0.2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"left-under\": {\n        type: \"Cone\",\n        props: {\n          position: [0, -1.5, 0],\n          rotation: [PI, 0, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#6B4513\",\n            metalness: 0.1,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.8,\n          height: 1.5,\n          radialSegments: 10,\n        },\n        children: [],\n      },\n      \"left-tree-t\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0.3, 0.8, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B5A2B\",\n            metalness: 0,\n            roughness: 1,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 0.04,\n          radiusBottom: 0.07,\n          height: 1,\n          radialSegments: 8,\n        },\n        children: [],\n      },\n      \"left-tree-c\": {\n        type: \"Sphere\",\n        props: {\n          position: [0.3, 1.6, 0],\n          rotation: null,\n          scale: [1, 1.2, 1],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#2d7a2d\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.4,\n          widthSegments: 12,\n          heightSegments: 8,\n        },\n        children: [],\n      },\n\n      \"island-right-float\": {\n        type: \"Float\",\n        props: {\n          position: [5.5, 0, -3],\n          rotation: null,\n          scale: null,\n          speed: 0.5,\n          rotationIntensity: 0.02,\n          floatIntensity: 0.4,\n          enabled: true,\n        },\n        children: [\"island-right\"],\n      },\n      \"island-right\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"right-base\",\n          \"right-top\",\n          \"right-under\",\n          \"right-tree-t\",\n          \"right-tree-c\",\n        ],\n      },\n      \"right-base\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, -0.3, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 2,\n          radiusBottom: 1,\n          height: 1.5,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"right-top\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.4, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#4a8c3f\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 2.05,\n          radiusBottom: 2,\n          height: 0.2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"right-under\": {\n        type: \"Cone\",\n        props: {\n          position: [0, -2, 0],\n          rotation: [PI, 0, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#6B4513\",\n            metalness: 0.1,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 1,\n          height: 2,\n          radialSegments: 10,\n        },\n        children: [],\n      },\n      \"right-tree-t\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-0.5, 0.9, 0.3],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B5A2B\",\n            metalness: 0,\n            roughness: 1,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 0.05,\n          radiusBottom: 0.09,\n          height: 1.3,\n          radialSegments: 8,\n        },\n        children: [],\n      },\n      \"right-tree-c\": {\n        type: \"Sphere\",\n        props: {\n          position: [-0.5, 2, 0.3],\n          rotation: null,\n          scale: [1, 1.3, 1],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#3a8c3a\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.5,\n          widthSegments: 12,\n          heightSegments: 8,\n        },\n        children: [],\n      },\n\n      \"island-far-float\": {\n        type: \"Float\",\n        props: {\n          position: [2, 3, -10],\n          rotation: null,\n          scale: [0.7, 0.7, 0.7],\n          speed: 0.3,\n          rotationIntensity: 0.01,\n          floatIntensity: 0.3,\n          enabled: true,\n        },\n        children: [\"far-base\", \"far-top\", \"far-under\"],\n      },\n      \"far-base\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 2.5,\n          radiusBottom: 1.2,\n          height: 1.5,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"far-top\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.65, 0],\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#3a7a3a\",\n            metalness: 0,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radiusTop: 2.55,\n          radiusBottom: 2.5,\n          height: 0.2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"far-under\": {\n        type: \"Cone\",\n        props: {\n          position: [0, -1.8, 0],\n          rotation: [PI, 0, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#6B4513\",\n            metalness: 0.1,\n            roughness: 0.95,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 1.2,\n          height: 2,\n          radialSegments: 10,\n        },\n        children: [],\n      },\n\n      \"island-tiny1-float\": {\n        type: \"Float\",\n        props: {\n          position: [-3, 2.5, -5],\n          rotation: null,\n          scale: [0.4, 0.4, 0.4],\n          speed: 0.8,\n          rotationIntensity: 0.05,\n          floatIntensity: 0.8,\n          enabled: true,\n        },\n        children: [\"tiny1-rock\"],\n      },\n      \"tiny1-rock\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: [1, 0.6, 1],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 1,\n          widthSegments: 12,\n          heightSegments: 8,\n        },\n        children: [],\n      },\n      \"island-tiny2-float\": {\n        type: \"Float\",\n        props: {\n          position: [8, -0.5, 1],\n          rotation: null,\n          scale: [0.3, 0.3, 0.3],\n          speed: 1,\n          rotationIntensity: 0.08,\n          floatIntensity: 1,\n          enabled: true,\n        },\n        children: [\"tiny2-rock\"],\n      },\n      \"tiny2-rock\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: [1.2, 0.5, 0.8],\n          castShadow: true,\n          receiveShadow: false,\n          material: {\n            color: \"#7a5a10\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 1,\n          widthSegments: 12,\n          heightSegments: 8,\n        },\n        children: [],\n      },\n\n      \"bridge-1\": {\n        type: \"RoundedBox\",\n        props: {\n          position: [-3.57, -0.1, 1.19],\n          rotation: [0, 0.32, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          width: 3.5,\n          height: 0.5,\n          depth: 0.6,\n          radius: 0.05,\n          smoothness: 2,\n        },\n        children: [],\n      },\n      \"bridge-2\": {\n        type: \"RoundedBox\",\n        props: {\n          position: [3.19, -0.1, -1.74],\n          rotation: [0, 0.5, 0],\n          scale: null,\n          castShadow: true,\n          receiveShadow: true,\n          material: {\n            color: \"#8B6914\",\n            metalness: 0.1,\n            roughness: 0.9,\n            emissive: null,\n            emissiveIntensity: null,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          width: 3.0,\n          height: 0.5,\n          depth: 0.6,\n          radius: 0.05,\n          smoothness: 2,\n        },\n        children: [],\n      },\n\n      sparkles: {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 2, 0],\n          rotation: null,\n          scale: [20, 8, 20],\n          count: 120,\n          speed: 0.1,\n          opacity: 0.5,\n          color: \"#ffeeaa\",\n          size: 1.5,\n          noise: 1,\n        },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 8,\n          maxDistance: 30,\n          minPolarAngle: 0.2,\n          maxPolarAngle: 1.5,\n          autoRotate: true,\n          autoRotateSpeed: 0.3,\n          target: [0, 1, 0],\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/hyperspace-tunnel.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const hyperspaceTunnel: Scene = {\n  name: \"Hyperspace Tunnel\",\n  description:\n    \"Neon warp tunnel at breakneck speed -- bloom, camera shake, particles streaming past\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"controls\",\n          \"tunnel\",\n          \"stars\",\n          \"sparkles-core\",\n          \"sparkles-wide\",\n          \"ambient\",\n          \"center-light\",\n          \"shake\",\n          \"post\",\n          \"fog\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 0, 2],\n          rotation: null,\n          scale: null,\n          fov: 80,\n          near: 0.1,\n          far: 200,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableRotate: false,\n          enableZoom: false,\n          enablePan: false,\n        },\n        children: [],\n      },\n      tunnel: {\n        type: \"WarpTunnel\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          ringCount: 100,\n          radius: 3,\n          length: 50,\n          speed: 12,\n          tubeRadius: 0.025,\n          color1: \"#00ffff\",\n          color2: \"#ff00ff\",\n        },\n        children: [],\n      },\n      stars: {\n        type: \"Stars\",\n        props: {\n          radius: 80,\n          depth: 60,\n          count: 6000,\n          factor: 5,\n          saturation: 0.4,\n          fade: true,\n          speed: 3,\n        },\n        children: [],\n      },\n      \"sparkles-core\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -10],\n          rotation: null,\n          scale: [4, 4, 30],\n          count: 300,\n          speed: 3,\n          opacity: 1,\n          color: \"#00ffff\",\n          size: 2,\n          noise: 0.5,\n        },\n        children: [],\n      },\n      \"sparkles-wide\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -15],\n          rotation: null,\n          scale: [6, 6, 40],\n          count: 200,\n          speed: 4,\n          opacity: 0.7,\n          color: \"#ff44ff\",\n          size: 1.5,\n          noise: 1,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#110033\", intensity: 0.2 },\n        children: [],\n      },\n      \"center-light\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, -5],\n          rotation: null,\n          scale: null,\n          color: \"#00ccff\",\n          intensity: 30,\n          distance: 30,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      shake: {\n        type: \"CameraShake\",\n        props: {\n          intensity: 0.3,\n          maxYaw: 0.03,\n          maxPitch: 0.03,\n          maxRoll: 0.02,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 2.5,\n          luminanceThreshold: 0.1,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.3, darkness: 0.9 },\n        children: [],\n      },\n      fog: {\n        type: \"Fog\",\n        props: { color: \"#030008\", near: 5, far: 50 },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/index.ts",
    "content": "import type { Scene } from \"./_helpers\";\nimport { hyperspaceTunnel } from \"./hyperspace-tunnel\";\nimport { orbitalChaos } from \"./orbital-chaos\";\nimport { clockworkOrrery } from \"./clockwork-orrery\";\nimport { deepSeaAbyss } from \"./deep-sea-abyss\";\nimport { stormCell } from \"./storm-cell\";\nimport { perpetualMotion } from \"./perpetual-motion\";\nimport { floatingIslands } from \"./floating-islands\";\nimport { productShowroom } from \"./product-showroom\";\nimport { portalGallery } from \"./portal-gallery\";\nimport { pipes } from \"./pipes\";\nimport { mystify } from \"./mystify\";\nimport { starfield } from \"./starfield\";\nexport type { Scene };\n\nexport const scenes: Scene[] = [\n  hyperspaceTunnel,\n  orbitalChaos,\n  clockworkOrrery,\n  deepSeaAbyss,\n  stormCell,\n  perpetualMotion,\n  floatingIslands,\n  productShowroom,\n  portalGallery,\n  pipes,\n  mystify,\n  starfield,\n];\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/mystify.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const mystify: Scene = {\n  name: \"Mystify\",\n  description:\n    \"Neon wireframe torus knots orbiting in the void -- the Windows Mystify screensaver in 3D\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"ambient\",\n          \"shape1\",\n          \"shape2\",\n          \"shape3\",\n          \"shape4\",\n          \"shape5\",\n          \"shape6\",\n          \"shape7\",\n          \"post\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 0, 10],\n          rotation: null,\n          scale: null,\n          fov: 55,\n          near: 0.1,\n          far: 1000,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#111111\", intensity: 0.1 },\n        children: [],\n      },\n      shape1: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.5,\n          radius: 3,\n          tilt: 0.3,\n        },\n        children: [\"s1-spin\"],\n      },\n      \"s1-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 1.5,\n          axis: \"x\",\n        },\n        children: [\"s1-body\"],\n      },\n      \"s1-body\": {\n        type: \"TorusKnot\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.4,\n          tube: 0.02,\n          p: 2,\n          q: 3,\n          tubularSegments: 128,\n          radialSegments: 4,\n          material: {\n            color: \"#00ffff\",\n            emissive: \"#00ffff\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape2: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.4,\n          radius: 4,\n          tilt: -0.5,\n        },\n        children: [\"s2-spin\"],\n      },\n      \"s2-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -1,\n          axis: \"z\",\n        },\n        children: [\"s2-body\"],\n      },\n      \"s2-body\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.6,\n          tube: 0.015,\n          radialSegments: 4,\n          tubularSegments: 128,\n          material: {\n            color: \"#ff00ff\",\n            emissive: \"#ff00ff\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape3: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.7,\n          radius: 2.5,\n          tilt: 0.8,\n        },\n        children: [\"s3-spin\"],\n      },\n      \"s3-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 2,\n          axis: \"y\",\n        },\n        children: [\"s3-body\"],\n      },\n      \"s3-body\": {\n        type: \"TorusKnot\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.35,\n          tube: 0.015,\n          p: 3,\n          q: 2,\n          tubularSegments: 128,\n          radialSegments: 4,\n          material: {\n            color: \"#ffff00\",\n            emissive: \"#ffff00\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape4: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.6,\n          radius: 3.5,\n          tilt: -0.2,\n        },\n        children: [\"s4-spin\"],\n      },\n      \"s4-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 1.2,\n          axis: \"x\",\n        },\n        children: [\"s4-body\"],\n      },\n      \"s4-body\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.5,\n          tube: 0.012,\n          radialSegments: 4,\n          tubularSegments: 128,\n          material: {\n            color: \"#00ff44\",\n            emissive: \"#00ff44\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape5: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.3,\n          radius: 5,\n          tilt: 0.6,\n        },\n        children: [\"s5-spin\"],\n      },\n      \"s5-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -1.8,\n          axis: \"z\",\n        },\n        children: [\"s5-body\"],\n      },\n      \"s5-body\": {\n        type: \"TorusKnot\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.5,\n          tube: 0.018,\n          p: 2,\n          q: 5,\n          tubularSegments: 128,\n          radialSegments: 4,\n          material: {\n            color: \"#ff4400\",\n            emissive: \"#ff4400\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape6: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.8,\n          radius: 2,\n          tilt: -0.7,\n        },\n        children: [\"s6-spin\"],\n      },\n      \"s6-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 2.5,\n          axis: \"y\",\n        },\n        children: [\"s6-body\"],\n      },\n      \"s6-body\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.3,\n          tube: 0.01,\n          radialSegments: 4,\n          tubularSegments: 128,\n          material: {\n            color: \"#4488ff\",\n            emissive: \"#4488ff\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      shape7: {\n        type: \"Orbit\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.45,\n          radius: 4.5,\n          tilt: 1,\n        },\n        children: [\"s7-spin\"],\n      },\n      \"s7-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.8,\n          axis: \"x\",\n        },\n        children: [\"s7-body\"],\n      },\n      \"s7-body\": {\n        type: \"TorusKnot\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.45,\n          tube: 0.015,\n          p: 3,\n          q: 4,\n          tubularSegments: 128,\n          radialSegments: 4,\n          material: {\n            color: \"#ff88ff\",\n            emissive: \"#ff88ff\",\n            emissiveIntensity: 2,\n            wireframe: true,\n          },\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 3,\n          luminanceThreshold: 0.05,\n          luminanceSmoothing: 0.3,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.2, darkness: 0.8 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: true,\n          enableRotate: true,\n          minDistance: 4,\n          maxDistance: 18,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: true,\n          autoRotateSpeed: 0.3,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/orbital-chaos.ts",
    "content": "import { PI } from \"./_helpers\";\nimport type { Scene } from \"./_helpers\";\n\nexport const orbitalChaos: Scene = {\n  name: \"Orbital Chaos\",\n  description:\n    \"Everything in motion -- orbiting glass, spinning knots, morphing core, all with bloom\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"env\",\n          \"stars\",\n          \"ambient\",\n          \"core-light\",\n          \"accent-1\",\n          \"accent-2\",\n          \"core-pulse\",\n          \"orbit-1\",\n          \"orbit-2\",\n          \"orbit-3\",\n          \"orbit-4\",\n          \"orbit-5\",\n          \"orbit-6\",\n          \"ring-spin-fast\",\n          \"ring-spin-slow\",\n          \"sparkles\",\n          \"post\",\n          \"fog\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 2, 8],\n          fov: 50,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: { preset: \"night\", background: false, blur: 1, intensity: 0.15 },\n        children: [],\n      },\n      stars: {\n        type: \"Stars\",\n        props: {\n          radius: 120,\n          depth: 60,\n          count: 8000,\n          factor: 5,\n          saturation: 0.2,\n          fade: true,\n          speed: 0.5,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#110022\", intensity: 0.3 },\n        children: [],\n      },\n      \"core-light\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#ff44aa\",\n          intensity: 80,\n          distance: 25,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"accent-1\": {\n        type: \"PointLight\",\n        props: {\n          position: [5, 3, 5],\n          rotation: null,\n          scale: null,\n          color: \"#00ffff\",\n          intensity: 25,\n          distance: 20,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"accent-2\": {\n        type: \"PointLight\",\n        props: {\n          position: [-5, -2, -5],\n          rotation: null,\n          scale: null,\n          color: \"#ff00ff\",\n          intensity: 25,\n          distance: 20,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n\n      \"core-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.8,\n          min: 0.85,\n          max: 1.15,\n        },\n        children: [\"core-spin\"],\n      },\n      \"core-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.3,\n          axis: \"y\",\n        },\n        children: [\"core-blob\"],\n      },\n      \"core-blob\": {\n        type: \"DistortSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 1.2,\n          widthSegments: 64,\n          heightSegments: 32,\n          color: \"#ff2288\",\n          speed: 4,\n          distort: 0.6,\n          metalness: 0.1,\n          roughness: 0.1,\n        },\n        children: [],\n      },\n\n      \"orbit-1\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.8,\n          radius: 4,\n          tilt: 0.5,\n        },\n        children: [\"orb-1-spin\"],\n      },\n      \"orb-1-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 2,\n          axis: \"x\",\n        },\n        children: [\"orb-1\"],\n      },\n      \"orb-1\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.4,\n          widthSegments: 32,\n          heightSegments: 16,\n          color: \"#00ffff\",\n          transmission: 1,\n          thickness: 0.5,\n          roughness: 0,\n          chromaticAberration: 0.15,\n          ior: 1.8,\n          distortion: 0.1,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n\n      \"orbit-2\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0.5, 0],\n          rotation: null,\n          scale: null,\n          speed: -0.6,\n          radius: 5.5,\n          tilt: 1,\n        },\n        children: [\"orb-2-spin\"],\n      },\n      \"orb-2-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 3,\n          axis: \"z\",\n        },\n        children: [\"orb-2\"],\n      },\n      \"orb-2\": {\n        type: \"TorusKnot\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ff00ff\",\n            metalness: 0.9,\n            roughness: 0.1,\n            emissive: \"#ff00ff\",\n            emissiveIntensity: 0.8,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 0.35,\n          tube: 0.12,\n          tubularSegments: 64,\n          radialSegments: 8,\n          p: 2,\n          q: 3,\n        },\n        children: [],\n      },\n\n      \"orbit-3\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, -0.3, 0],\n          rotation: null,\n          scale: null,\n          speed: 1.2,\n          radius: 3.2,\n          tilt: 0.3,\n        },\n        children: [\"orb-3\"],\n      },\n      \"orb-3\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.3,\n          widthSegments: 32,\n          heightSegments: 16,\n          color: \"#ff8800\",\n          transmission: 1,\n          thickness: 0.4,\n          roughness: 0,\n          chromaticAberration: 0.1,\n          ior: 1.6,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n\n      \"orbit-4\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 1, 0],\n          rotation: null,\n          scale: null,\n          speed: -1.5,\n          radius: 6.5,\n          tilt: -0.8,\n        },\n        children: [\"orb-4-spin\"],\n      },\n      \"orb-4-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -4,\n          axis: \"y\",\n        },\n        children: [\"orb-4\"],\n      },\n      \"orb-4\": {\n        type: \"Sphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#00ff88\",\n            metalness: null,\n            roughness: null,\n            emissive: \"#00ff88\",\n            emissiveIntensity: 0.8,\n            opacity: null,\n            transparent: null,\n            wireframe: true,\n          },\n          radius: 0.35,\n          widthSegments: 16,\n          heightSegments: 12,\n        },\n        children: [],\n      },\n\n      \"orbit-5\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, -0.5, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.4,\n          radius: 8,\n          tilt: 0.2,\n        },\n        children: [\"orb-5-spin\"],\n      },\n      \"orb-5-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 1.5,\n          axis: \"z\",\n        },\n        children: [\"orb-5\"],\n      },\n      \"orb-5\": {\n        type: \"GlassBox\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          width: 0.5,\n          height: 0.5,\n          depth: 0.5,\n          color: \"#ff44ff\",\n          transmission: 1,\n          thickness: 0.4,\n          roughness: 0,\n          chromaticAberration: 0.08,\n          ior: 1.7,\n          distortion: 0,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n\n      \"orbit-6\": {\n        type: \"Orbit\",\n        props: {\n          position: [0, 0.8, 0],\n          rotation: null,\n          scale: null,\n          speed: 2,\n          radius: 2.2,\n          tilt: 1.5,\n        },\n        children: [\"orb-6\"],\n      },\n      \"orb-6\": {\n        type: \"DistortSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          radius: 0.25,\n          widthSegments: 32,\n          heightSegments: 16,\n          color: \"#ffcc00\",\n          speed: 5,\n          distort: 0.7,\n          metalness: 0.5,\n          roughness: 0.1,\n        },\n        children: [],\n      },\n\n      \"ring-spin-fast\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [0.3, 0, 0],\n          scale: null,\n          speed: 0.5,\n          axis: \"y\",\n        },\n        children: [\"ring-inner\"],\n      },\n      \"ring-inner\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [PI / 2, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#00ffff\",\n            metalness: 0.9,\n            roughness: 0.1,\n            emissive: \"#00ccff\",\n            emissiveIntensity: 0.5,\n            opacity: 0.3,\n            transparent: true,\n            wireframe: null,\n          },\n          radius: 9,\n          tube: 0.015,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"ring-spin-slow\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: [-0.5, 0, 0.3],\n          scale: null,\n          speed: -0.3,\n          axis: \"y\",\n        },\n        children: [\"ring-outer\"],\n      },\n      \"ring-outer\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [PI / 2, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ff00ff\",\n            metalness: 0.9,\n            roughness: 0.1,\n            emissive: \"#ff00aa\",\n            emissiveIntensity: 0.3,\n            opacity: 0.2,\n            transparent: true,\n            wireframe: null,\n          },\n          radius: 11,\n          tube: 0.012,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n\n      sparkles: {\n        type: \"Sparkles\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: [18, 18, 18],\n          count: 400,\n          speed: 0.4,\n          opacity: 0.5,\n          color: \"#ffddaa\",\n          size: 1.2,\n          noise: 2,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 2,\n          luminanceThreshold: 0.15,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.4, darkness: 0.7 },\n        children: [],\n      },\n      fog: {\n        type: \"Fog\",\n        props: { color: \"#030008\", near: 10, far: 40 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 5,\n          maxDistance: 25,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: true,\n          autoRotateSpeed: 0.4,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/perpetual-motion.ts",
    "content": "import { PI } from \"./_helpers\";\nimport type { Scene } from \"./_helpers\";\n\nexport const perpetualMotion: Scene = {\n  name: \"Perpetual Motion Machine\",\n  description:\n    \"A gyroscope/gimbal with 3 nested spinning rings on different axes, with a perfectly still glass sphere floating at the center. Clean, minimal, mesmerizing.\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"env\",\n          \"ambient\",\n          \"key\",\n          \"fill\",\n          \"outer-spin\",\n          \"sparkles\",\n          \"floor\",\n          \"shadows\",\n          \"post\",\n          \"controls\",\n        ],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"studio\",\n          background: false,\n          blur: null,\n          intensity: 1,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#ffffff\", intensity: 0.3 },\n        children: [],\n      },\n      key: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [5, 8, 5],\n          rotation: null,\n          scale: null,\n          color: \"#ffffff\",\n          intensity: 2,\n          castShadow: true,\n        },\n        children: [],\n      },\n      fill: {\n        type: \"PointLight\",\n        props: {\n          position: [-3, 4, -3],\n          rotation: null,\n          scale: null,\n          color: \"#aaccff\",\n          intensity: 10,\n          distance: 15,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"outer-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.4,\n          axis: \"y\",\n        },\n        children: [\"outer-ring\", \"mid-spin\"],\n      },\n      \"outer-ring\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [PI / 2, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#cccccc\",\n            metalness: 1,\n            roughness: 0.05,\n            emissive: \"#ffffff\",\n            emissiveIntensity: 0.05,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 4,\n          tube: 0.06,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"mid-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: -0.8,\n          axis: \"x\",\n        },\n        children: [\"mid-ring\", \"inner-spin\"],\n      },\n      \"mid-ring\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [PI / 2, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#ffcc44\",\n            metalness: 0.95,\n            roughness: 0.08,\n            emissive: \"#cc9900\",\n            emissiveIntensity: 0.1,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 3,\n          tube: 0.05,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"inner-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 1.5,\n          axis: \"z\",\n        },\n        children: [\"inner-ring\", \"center-float\"],\n      },\n      \"inner-ring\": {\n        type: \"Torus\",\n        props: {\n          position: null,\n          rotation: [PI / 2, 0, 0],\n          scale: null,\n          castShadow: false,\n          receiveShadow: false,\n          material: {\n            color: \"#cc8844\",\n            metalness: 0.9,\n            roughness: 0.1,\n            emissive: \"#996633\",\n            emissiveIntensity: 0.1,\n            opacity: null,\n            transparent: null,\n            wireframe: null,\n          },\n          radius: 2,\n          tube: 0.04,\n          radialSegments: null,\n          tubularSegments: 128,\n        },\n        children: [],\n      },\n      \"center-float\": {\n        type: \"Float\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.3,\n          rotationIntensity: 0,\n          floatIntensity: 0.1,\n          enabled: true,\n        },\n        children: [\"center-orb\"],\n      },\n      \"center-orb\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: true,\n          receiveShadow: false,\n          radius: 0.8,\n          widthSegments: 64,\n          heightSegments: 32,\n          color: \"#ffffff\",\n          transmission: 1,\n          thickness: 1,\n          roughness: 0,\n          chromaticAberration: 0.12,\n          ior: 2,\n          distortion: 0.1,\n          distortionScale: 0.4,\n          temporalDistortion: 0.4,\n          samples: 10,\n          resolution: 256,\n        },\n        children: [],\n      },\n      sparkles: {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: [10, 10, 10],\n          count: 100,\n          speed: 0.1,\n          opacity: 0.2,\n          color: \"#ccddff\",\n          size: 0.8,\n          noise: 2,\n        },\n        children: [],\n      },\n      floor: {\n        type: \"ReflectorPlane\",\n        props: {\n          position: [0, -4.5, 0],\n          rotation: [-PI / 2, 0, 0],\n          scale: null,\n          width: 25,\n          height: 25,\n          color: \"#1a1a2a\",\n          resolution: 1024,\n          blur: 400,\n          mirror: 0.7,\n          mixBlur: 10,\n          mixStrength: 2,\n          depthScale: 0.2,\n          metalness: 0.5,\n          roughness: 1,\n        },\n        children: [],\n      },\n      shadows: {\n        type: \"ContactShadows\",\n        props: {\n          position: [0, -4.5, 0],\n          rotation: null,\n          scale: null,\n          opacity: 0.3,\n          width: 12,\n          height: 12,\n          blur: 2.5,\n          near: null,\n          far: 10,\n          smooth: true,\n          resolution: 512,\n          frames: null,\n          color: \"#000000\",\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 0.8,\n          luminanceThreshold: 0.25,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.5, darkness: 0.4 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 5,\n          maxDistance: 16,\n          minPolarAngle: 0.3,\n          maxPolarAngle: 1.5,\n          autoRotate: true,\n          autoRotateSpeed: 0.3,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/pipes.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nconst goldMaterial = {\n  color: \"#cc8833\",\n  metalness: 1,\n  roughness: 0.05,\n};\n\nconst blueMaterial = {\n  color: \"#3388cc\",\n  metalness: 1,\n  roughness: 0.05,\n};\n\nconst magentaMaterial = {\n  color: \"#cc3388\",\n  metalness: 1,\n  roughness: 0.05,\n};\n\nexport const pipes: Scene = {\n  name: \"Pipes\",\n  description:\n    \"Classic 3D Pipes screensaver -- chrome pipes snaking through space at right angles\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"camera\",\n          \"env\",\n          \"ambient\",\n          \"directional\",\n          \"pipe-system\",\n          \"post\",\n          \"fog\",\n          \"controls\",\n        ],\n      },\n      camera: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 3, 12],\n          rotation: null,\n          scale: null,\n          fov: 50,\n          near: null,\n          far: null,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"warehouse\",\n          background: false,\n          blur: 0.5,\n          intensity: 1,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#ffffff\", intensity: 0.3 },\n        children: [],\n      },\n      directional: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [5, 10, 5],\n          rotation: null,\n          scale: null,\n          color: \"#ffffff\",\n          intensity: 1,\n          castShadow: null,\n        },\n        children: [],\n      },\n      \"pipe-system\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.1,\n          axis: \"y\",\n        },\n        children: [\"pipe1-group\", \"pipe2-group\", \"pipe3-group\"],\n      },\n      \"pipe1-group\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"p1-j0\",\n          \"p1-s1\",\n          \"p1-j1\",\n          \"p1-s2\",\n          \"p1-j2\",\n          \"p1-s3\",\n          \"p1-j3\",\n          \"p1-s4\",\n          \"p1-j4\",\n          \"p1-s5\",\n          \"p1-j5\",\n          \"p1-s6\",\n          \"p1-j6\",\n        ],\n      },\n      \"p1-j0\": {\n        type: \"Sphere\",\n        props: {\n          position: [-3, -3, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j1\": {\n        type: \"Sphere\",\n        props: {\n          position: [-3, 0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j2\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j3\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 2, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j4\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 2, -3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j5\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 0, -3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-j6\": {\n        type: \"Sphere\",\n        props: {\n          position: [-1, 0, -3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p1-s1\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-3, -1.5, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p1-s2\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-1, 0, 0],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 4,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p1-s3\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, 1, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p1-s4\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, 2, -1.5],\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p1-s5\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, 1, -3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p1-s6\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0, -3],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: goldMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"pipe2-group\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"p2-j0\",\n          \"p2-s1\",\n          \"p2-j1\",\n          \"p2-s2\",\n          \"p2-j2\",\n          \"p2-s3\",\n          \"p2-j3\",\n          \"p2-s4\",\n          \"p2-j4\",\n          \"p2-s5\",\n          \"p2-j5\",\n          \"p2-s6\",\n          \"p2-j6\",\n        ],\n      },\n      \"p2-j0\": {\n        type: \"Sphere\",\n        props: {\n          position: [2, -3, 2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j1\": {\n        type: \"Sphere\",\n        props: {\n          position: [2, 1, 2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j2\": {\n        type: \"Sphere\",\n        props: {\n          position: [2, 1, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j3\": {\n        type: \"Sphere\",\n        props: {\n          position: [-1, 1, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j4\": {\n        type: \"Sphere\",\n        props: {\n          position: [-1, 4, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j5\": {\n        type: \"Sphere\",\n        props: {\n          position: [-3, 4, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-j6\": {\n        type: \"Sphere\",\n        props: {\n          position: [-3, 4, 1],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p2-s1\": {\n        type: \"Cylinder\",\n        props: {\n          position: [2, -1, 2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 4,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p2-s2\": {\n        type: \"Cylinder\",\n        props: {\n          position: [2, 1, 0],\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 4,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p2-s3\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0.5, 1, -2],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p2-s4\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-1, 2.5, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p2-s5\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-2, 4, -2],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p2-s6\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-3, 4, -0.5],\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: blueMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"pipe3-group\": {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"p3-j0\",\n          \"p3-s1\",\n          \"p3-j1\",\n          \"p3-s2\",\n          \"p3-j2\",\n          \"p3-s3\",\n          \"p3-j3\",\n          \"p3-s4\",\n          \"p3-j4\",\n          \"p3-s5\",\n          \"p3-j5\",\n        ],\n      },\n      \"p3-j0\": {\n        type: \"Sphere\",\n        props: {\n          position: [-2, 3, 3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-j1\": {\n        type: \"Sphere\",\n        props: {\n          position: [-2, -2, 3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-j2\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, -2, 3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-j3\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, -2, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-j4\": {\n        type: \"Sphere\",\n        props: {\n          position: [1, 0, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-j5\": {\n        type: \"Sphere\",\n        props: {\n          position: [-3, 0, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n        },\n        children: [],\n      },\n      \"p3-s1\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-2, 0.5, 3],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 5,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p3-s2\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-0.5, -2, 3],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 3,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p3-s3\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, -2, 0.5],\n          rotation: [1.5708, 0, 0],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 5,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p3-s4\": {\n        type: \"Cylinder\",\n        props: {\n          position: [1, -1, -2],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 2,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      \"p3-s5\": {\n        type: \"Cylinder\",\n        props: {\n          position: [-1, 0, -2],\n          rotation: [0, 0, 1.5708],\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          material: magentaMaterial,\n          radiusTop: 0.08,\n          radiusBottom: 0.08,\n          height: 4,\n          radialSegments: 12,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 0.4,\n          luminanceThreshold: 0.4,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.3, darkness: 0.4 },\n        children: [],\n      },\n      fog: {\n        type: \"Fog\",\n        props: { color: \"#111111\", near: 10, far: 30 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: null,\n          enablePan: null,\n          enableRotate: null,\n          minDistance: 5,\n          maxDistance: 20,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: true,\n          autoRotateSpeed: 0.6,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/portal-gallery.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const portalGallery: Scene = {\n  name: \"Portal Gallery\",\n  description:\n    \"Three framed portals into different worlds -- sunset clouds, starfield, and enchanted forest\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"env\",\n          \"ambient\",\n          \"directional\",\n          \"floor\",\n          \"shadows\",\n          \"frame1\",\n          \"frame2\",\n          \"frame3\",\n          \"gallery-sparkles\",\n          \"post\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 2, 7],\n          rotation: null,\n          scale: null,\n          fov: 50,\n          near: 0.1,\n          far: 200,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"lobby\",\n          background: false,\n          blur: 0,\n          intensity: 0.4,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#e8e0d8\", intensity: 0.5 },\n        children: [],\n      },\n      directional: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [5, 8, 3],\n          rotation: null,\n          scale: null,\n          color: \"#ffffff\",\n          intensity: 1,\n          castShadow: true,\n        },\n        children: [],\n      },\n      floor: {\n        type: \"ReflectorPlane\",\n        props: {\n          position: [0, 0, 0],\n          rotation: [-1.5708, 0, 0],\n          scale: null,\n          width: 30,\n          height: 30,\n          color: \"#2a2a2a\",\n          resolution: 1024,\n          blur: 400,\n          mirror: 0.4,\n          mixBlur: 10,\n          mixStrength: 1.5,\n          metalness: 0.5,\n          roughness: 0.8,\n        },\n        children: [],\n      },\n      shadows: {\n        type: \"ContactShadows\",\n        props: {\n          position: [0, 0.01, 0],\n          rotation: null,\n          scale: null,\n          opacity: 0.4,\n          width: 20,\n          height: 20,\n          blur: 2.5,\n          far: 8,\n          color: \"#000000\",\n        },\n        children: [],\n      },\n\n      // --- PORTAL 1 (left, sunset world) ---\n      frame1: {\n        type: \"Group\",\n        props: {\n          position: [-3.5, 2, -2],\n          rotation: [0, 0.3, 0],\n          scale: null,\n        },\n        children: [\n          \"frame1-top\",\n          \"frame1-bottom\",\n          \"frame1-left\",\n          \"frame1-right\",\n          \"portal1-mesh\",\n        ],\n      },\n      \"frame1-top\": {\n        type: \"Box\",\n        props: {\n          position: [0, 1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#8B7355\", metalness: 0.3, roughness: 0.6 },\n        },\n        children: [],\n      },\n      \"frame1-bottom\": {\n        type: \"Box\",\n        props: {\n          position: [0, -1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#8B7355\", metalness: 0.3, roughness: 0.6 },\n        },\n        children: [],\n      },\n      \"frame1-left\": {\n        type: \"Box\",\n        props: {\n          position: [-1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#8B7355\", metalness: 0.3, roughness: 0.6 },\n        },\n        children: [],\n      },\n      \"frame1-right\": {\n        type: \"Box\",\n        props: {\n          position: [1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#8B7355\", metalness: 0.3, roughness: 0.6 },\n        },\n        children: [],\n      },\n      \"portal1-mesh\": {\n        type: \"Plane\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          width: 2,\n          height: 3,\n        },\n        children: [\"portal1-mat\"],\n      },\n      \"portal1-mat\": {\n        type: \"MeshPortalMaterial\",\n        props: { blend: 0, blur: 0.5, resolution: 512 },\n        children: [\n          \"p1-sky\",\n          \"p1-sun-light\",\n          \"p1-cloud1\",\n          \"p1-cloud2\",\n          \"p1-cloud3\",\n          \"p1-ground\",\n        ],\n      },\n      \"p1-sky\": {\n        type: \"Sky\",\n        props: {\n          distance: null,\n          sunPosition: [100, 10, 50],\n          inclination: null,\n          azimuth: null,\n          rayleigh: 2,\n          turbidity: 10,\n          mieCoefficient: 0.005,\n          mieDirectionalG: null,\n        },\n        children: [],\n      },\n      \"p1-sun-light\": {\n        type: \"DirectionalLight\",\n        props: {\n          position: [5, 5, 3],\n          rotation: null,\n          scale: null,\n          color: \"#ffaa55\",\n          intensity: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"p1-cloud1\": {\n        type: \"Cloud\",\n        props: {\n          position: [-3, 4, -5],\n          rotation: null,\n          scale: null,\n          seed: 1,\n          segments: 20,\n          bounds: [6, 2, 3],\n          opacity: 0.6,\n          color: \"#ffddaa\",\n          speed: 0.1,\n          volume: 5,\n          fade: null,\n          growth: null,\n        },\n        children: [],\n      },\n      \"p1-cloud2\": {\n        type: \"Cloud\",\n        props: {\n          position: [4, 5, -8],\n          rotation: null,\n          scale: null,\n          seed: 7,\n          segments: 25,\n          bounds: [8, 2, 4],\n          opacity: 0.5,\n          color: \"#ffccaa\",\n          speed: 0.08,\n          volume: 6,\n          fade: null,\n          growth: null,\n        },\n        children: [],\n      },\n      \"p1-cloud3\": {\n        type: \"Cloud\",\n        props: {\n          position: [0, 3.5, -4],\n          rotation: null,\n          scale: null,\n          seed: 14,\n          segments: 15,\n          bounds: [5, 1.5, 2.5],\n          opacity: 0.7,\n          color: \"#ffeedd\",\n          speed: 0.12,\n          volume: 4,\n          fade: null,\n          growth: null,\n        },\n        children: [],\n      },\n      \"p1-ground\": {\n        type: \"Plane\",\n        props: {\n          position: [0, -2, 0],\n          rotation: [-1.5708, 0, 0],\n          scale: null,\n          width: 30,\n          height: 30,\n          material: { color: \"#886644\", roughness: 1 },\n        },\n        children: [],\n      },\n\n      // --- PORTAL 2 (center, night/stars world) ---\n      frame2: {\n        type: \"Group\",\n        props: {\n          position: [0, 2, -3],\n          rotation: null,\n          scale: null,\n        },\n        children: [\n          \"frame2-top\",\n          \"frame2-bottom\",\n          \"frame2-left\",\n          \"frame2-right\",\n          \"portal2-mesh\",\n        ],\n      },\n      \"frame2-top\": {\n        type: \"Box\",\n        props: {\n          position: [0, 1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#555566\", metalness: 0.8, roughness: 0.2 },\n        },\n        children: [],\n      },\n      \"frame2-bottom\": {\n        type: \"Box\",\n        props: {\n          position: [0, -1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#555566\", metalness: 0.8, roughness: 0.2 },\n        },\n        children: [],\n      },\n      \"frame2-left\": {\n        type: \"Box\",\n        props: {\n          position: [-1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#555566\", metalness: 0.8, roughness: 0.2 },\n        },\n        children: [],\n      },\n      \"frame2-right\": {\n        type: \"Box\",\n        props: {\n          position: [1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#555566\", metalness: 0.8, roughness: 0.2 },\n        },\n        children: [],\n      },\n      \"portal2-mesh\": {\n        type: \"Plane\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          width: 2,\n          height: 3,\n        },\n        children: [\"portal2-mat\"],\n      },\n      \"portal2-mat\": {\n        type: \"MeshPortalMaterial\",\n        props: { blend: 0, blur: 0.5, resolution: 512 },\n        children: [\n          \"p2-stars\",\n          \"p2-ambient\",\n          \"p2-nebula1\",\n          \"p2-nebula2\",\n          \"p2-sparkles\",\n        ],\n      },\n      \"p2-stars\": {\n        type: \"Stars\",\n        props: {\n          radius: 80,\n          depth: 40,\n          count: 6000,\n          factor: 5,\n          saturation: 0.5,\n          fade: true,\n          speed: 0.3,\n        },\n        children: [],\n      },\n      \"p2-ambient\": {\n        type: \"AmbientLight\",\n        props: { color: \"#111133\", intensity: 0.3 },\n        children: [],\n      },\n      \"p2-nebula1\": {\n        type: \"Sphere\",\n        props: {\n          position: [3, 2, -8],\n          rotation: null,\n          scale: null,\n          radius: 2,\n          widthSegments: null,\n          heightSegments: null,\n          material: {\n            color: \"#4400aa\",\n            emissive: \"#4400aa\",\n            emissiveIntensity: 0.5,\n            transparent: true,\n            opacity: 0.3,\n          },\n        },\n        children: [],\n      },\n      \"p2-nebula2\": {\n        type: \"Sphere\",\n        props: {\n          position: [-4, -1, -10],\n          rotation: null,\n          scale: null,\n          radius: 3,\n          widthSegments: null,\n          heightSegments: null,\n          material: {\n            color: \"#aa0044\",\n            emissive: \"#aa0044\",\n            emissiveIntensity: 0.4,\n            transparent: true,\n            opacity: 0.25,\n          },\n        },\n        children: [],\n      },\n      \"p2-sparkles\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -5],\n          rotation: null,\n          scale: [10, 10, 10],\n          count: 200,\n          speed: 0.2,\n          opacity: 0.8,\n          color: \"#aabbff\",\n          size: 2,\n          noise: 3,\n        },\n        children: [],\n      },\n\n      // --- PORTAL 3 (right, forest/nature world) ---\n      frame3: {\n        type: \"Group\",\n        props: {\n          position: [3.5, 2, -2],\n          rotation: [0, -0.3, 0],\n          scale: null,\n        },\n        children: [\n          \"frame3-top\",\n          \"frame3-bottom\",\n          \"frame3-left\",\n          \"frame3-right\",\n          \"portal3-mesh\",\n        ],\n      },\n      \"frame3-top\": {\n        type: \"Box\",\n        props: {\n          position: [0, 1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#6B4423\", metalness: 0.1, roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"frame3-bottom\": {\n        type: \"Box\",\n        props: {\n          position: [0, -1.55, 0],\n          rotation: null,\n          scale: null,\n          width: 2.3,\n          height: 0.15,\n          depth: 0.15,\n          material: { color: \"#6B4423\", metalness: 0.1, roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"frame3-left\": {\n        type: \"Box\",\n        props: {\n          position: [-1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#6B4423\", metalness: 0.1, roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"frame3-right\": {\n        type: \"Box\",\n        props: {\n          position: [1.075, 0, 0],\n          rotation: null,\n          scale: null,\n          width: 0.15,\n          height: 3.25,\n          depth: 0.15,\n          material: { color: \"#6B4423\", metalness: 0.1, roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"portal3-mesh\": {\n        type: \"Plane\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          width: 2,\n          height: 3,\n        },\n        children: [\"portal3-mat\"],\n      },\n      \"portal3-mat\": {\n        type: \"MeshPortalMaterial\",\n        props: { blend: 0, blur: 0.5, resolution: 512 },\n        children: [\n          \"p3-env\",\n          \"p3-ambient\",\n          \"p3-sun\",\n          \"p3-tree1\",\n          \"p3-tree2\",\n          \"p3-tree3\",\n          \"p3-tree4\",\n          \"p3-tree5\",\n          \"p3-ground\",\n          \"p3-fireflies\",\n        ],\n      },\n      \"p3-env\": {\n        type: \"Environment\",\n        props: { preset: \"forest\", background: false, blur: 0, intensity: 0.8 },\n        children: [],\n      },\n      \"p3-ambient\": {\n        type: \"AmbientLight\",\n        props: { color: \"#334422\", intensity: 0.4 },\n        children: [],\n      },\n      \"p3-sun\": {\n        type: \"DirectionalLight\",\n        props: {\n          position: [-3, 8, 2],\n          rotation: null,\n          scale: null,\n          color: \"#aaffaa\",\n          intensity: 1.5,\n          castShadow: true,\n        },\n        children: [],\n      },\n      \"p3-tree1\": {\n        type: \"Group\",\n        props: { position: [-2, -2, -4], rotation: null, scale: null },\n        children: [\"p3-t1-trunk\", \"p3-t1-leaves\"],\n      },\n      \"p3-t1-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.6, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.08,\n          radiusBottom: 0.12,\n          height: 1.2,\n          radialSegments: 8,\n          material: { color: \"#4a3520\", roughness: 0.9 },\n        },\n        children: [],\n      },\n      \"p3-t1-leaves\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 1.8, 0],\n          rotation: null,\n          scale: null,\n          radius: 0.6,\n          height: 1.5,\n          radialSegments: 8,\n          material: { color: \"#2d5a1e\", roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"p3-tree2\": {\n        type: \"Group\",\n        props: { position: [1.5, -2, -6], rotation: null, scale: null },\n        children: [\"p3-t2-trunk\", \"p3-t2-leaves\"],\n      },\n      \"p3-t2-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.8, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.1,\n          radiusBottom: 0.15,\n          height: 1.6,\n          radialSegments: 8,\n          material: { color: \"#3d2b15\", roughness: 0.9 },\n        },\n        children: [],\n      },\n      \"p3-t2-leaves\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 2.3, 0],\n          rotation: null,\n          scale: null,\n          radius: 0.8,\n          height: 2,\n          radialSegments: 8,\n          material: { color: \"#1e4a10\", roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"p3-tree3\": {\n        type: \"Group\",\n        props: { position: [-0.5, -2, -3], rotation: null, scale: null },\n        children: [\"p3-t3-trunk\", \"p3-t3-leaves\"],\n      },\n      \"p3-t3-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.5, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.06,\n          radiusBottom: 0.1,\n          height: 1,\n          radialSegments: 8,\n          material: { color: \"#4a3520\", roughness: 0.9 },\n        },\n        children: [],\n      },\n      \"p3-t3-leaves\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 1.5, 0],\n          rotation: null,\n          scale: null,\n          radius: 0.5,\n          height: 1.2,\n          radialSegments: 8,\n          material: { color: \"#3a6b2a\", roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"p3-tree4\": {\n        type: \"Group\",\n        props: { position: [3, -2, -8], rotation: null, scale: null },\n        children: [\"p3-t4-trunk\", \"p3-t4-leaves\"],\n      },\n      \"p3-t4-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 1, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.12,\n          radiusBottom: 0.18,\n          height: 2,\n          radialSegments: 8,\n          material: { color: \"#3d2b15\", roughness: 0.9 },\n        },\n        children: [],\n      },\n      \"p3-t4-leaves\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 2.8, 0],\n          rotation: null,\n          scale: null,\n          radius: 1,\n          height: 2.5,\n          radialSegments: 8,\n          material: { color: \"#2d5a1e\", roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"p3-tree5\": {\n        type: \"Group\",\n        props: { position: [-3.5, -2, -7], rotation: null, scale: null },\n        children: [\"p3-t5-trunk\", \"p3-t5-leaves\"],\n      },\n      \"p3-t5-trunk\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.7, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.09,\n          radiusBottom: 0.13,\n          height: 1.4,\n          radialSegments: 8,\n          material: { color: \"#4a3520\", roughness: 0.9 },\n        },\n        children: [],\n      },\n      \"p3-t5-leaves\": {\n        type: \"Cone\",\n        props: {\n          position: [0, 2, 0],\n          rotation: null,\n          scale: null,\n          radius: 0.7,\n          height: 1.8,\n          radialSegments: 8,\n          material: { color: \"#1e4a10\", roughness: 0.8 },\n        },\n        children: [],\n      },\n      \"p3-ground\": {\n        type: \"Plane\",\n        props: {\n          position: [0, -2, 0],\n          rotation: [-1.5708, 0, 0],\n          scale: null,\n          width: 30,\n          height: 30,\n          material: { color: \"#3a5a2a\", roughness: 1 },\n        },\n        children: [],\n      },\n      \"p3-fireflies\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -5],\n          rotation: null,\n          scale: [6, 4, 6],\n          count: 80,\n          speed: 0.3,\n          opacity: 0.7,\n          color: \"#aaff66\",\n          size: 1.5,\n          noise: 2,\n        },\n        children: [],\n      },\n\n      \"gallery-sparkles\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 3, 0],\n          rotation: null,\n          scale: [12, 5, 10],\n          count: 40,\n          speed: 0.05,\n          opacity: 0.2,\n          color: \"#ffffff\",\n          size: 0.3,\n          noise: null,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 0.6,\n          luminanceThreshold: 0.3,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.4, darkness: 0.4 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 4,\n          maxDistance: 16,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: true,\n          autoRotateSpeed: 0.3,\n          target: [0, 1.8, -2],\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/product-showroom.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const productShowroom: Scene = {\n  name: \"Product Showroom\",\n  description:\n    \"Sleek glass torus knot on a pedestal with studio lighting, reflections, and contact shadows\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"camera\",\n          \"env\",\n          \"backdrop\",\n          \"ambient\",\n          \"key-light\",\n          \"fill-light\",\n          \"rim-light\",\n          \"accent-light\",\n          \"pedestal\",\n          \"hero-float\",\n          \"accent1\",\n          \"accent2\",\n          \"accent3\",\n          \"floor\",\n          \"shadows\",\n          \"sparkles\",\n          \"fx\",\n          \"controls\",\n        ],\n      },\n      camera: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 2, 6],\n          rotation: null,\n          scale: null,\n          fov: 40,\n          near: null,\n          far: null,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      env: {\n        type: \"Environment\",\n        props: {\n          preset: \"studio\",\n          background: false,\n          blur: 0.5,\n          intensity: 0.8,\n        },\n        children: [],\n      },\n      backdrop: {\n        type: \"Backdrop\",\n        props: {\n          position: [0, -0.5, -3],\n          rotation: null,\n          scale: [25, 10, 8],\n          floor: 0.3,\n          segments: 30,\n          receiveShadow: true,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#e8e0f0\", intensity: 0.25 },\n        children: [],\n      },\n      \"key-light\": {\n        type: \"SpotLight\",\n        props: {\n          position: [5, 8, 3],\n          rotation: null,\n          scale: null,\n          color: \"#ffffff\",\n          intensity: 80,\n          distance: 25,\n          decay: null,\n          angle: 0.5,\n          penumbra: 0.8,\n          castShadow: true,\n        },\n        children: [],\n      },\n      \"fill-light\": {\n        type: \"SpotLight\",\n        props: {\n          position: [-4, 5, 2],\n          rotation: null,\n          scale: null,\n          color: \"#aaccff\",\n          intensity: 30,\n          distance: 20,\n          decay: null,\n          angle: 0.6,\n          penumbra: 1,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"rim-light\": {\n        type: \"SpotLight\",\n        props: {\n          position: [0, 3, -5],\n          rotation: null,\n          scale: null,\n          color: \"#ffddcc\",\n          intensity: 40,\n          distance: 15,\n          decay: null,\n          angle: 0.4,\n          penumbra: 0.5,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"accent-light\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0.5, 0],\n          rotation: null,\n          scale: null,\n          color: \"#ffffff\",\n          intensity: 5,\n          distance: 4,\n          decay: null,\n          castShadow: null,\n        },\n        children: [],\n      },\n      pedestal: {\n        type: \"Group\",\n        props: { position: [0, 0, 0], rotation: null, scale: null },\n        children: [\"ped-base\", \"ped-column\", \"ped-top\"],\n      },\n      \"ped-base\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.05, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 1.2,\n          radiusBottom: 1.3,\n          height: 0.1,\n          radialSegments: 48,\n          castShadow: null,\n          receiveShadow: null,\n          material: {\n            color: \"#1a1a1a\",\n            metalness: 0.9,\n            roughness: 0.1,\n          },\n        },\n        children: [],\n      },\n      \"ped-column\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.45, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.6,\n          radiusBottom: 0.8,\n          height: 0.7,\n          radialSegments: 48,\n          castShadow: null,\n          receiveShadow: null,\n          material: {\n            color: \"#111111\",\n            metalness: 0.95,\n            roughness: 0.05,\n          },\n        },\n        children: [],\n      },\n      \"ped-top\": {\n        type: \"Cylinder\",\n        props: {\n          position: [0, 0.85, 0],\n          rotation: null,\n          scale: null,\n          radiusTop: 0.9,\n          radiusBottom: 0.7,\n          height: 0.1,\n          radialSegments: 48,\n          castShadow: null,\n          receiveShadow: null,\n          material: {\n            color: \"#1a1a1a\",\n            metalness: 0.9,\n            roughness: 0.1,\n          },\n        },\n        children: [],\n      },\n      \"hero-float\": {\n        type: \"Float\",\n        props: {\n          position: [0, 2.2, 0],\n          rotation: null,\n          scale: null,\n          speed: 1.5,\n          rotationIntensity: 0.3,\n          floatIntensity: 0.4,\n          enabled: true,\n        },\n        children: [\"hero-spin\"],\n      },\n      \"hero-spin\": {\n        type: \"Spin\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          speed: 0.3,\n          axis: \"y\",\n        },\n        children: [\"hero-object\"],\n      },\n      \"hero-object\": {\n        type: \"TorusKnot\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.6,\n          tube: 0.22,\n          p: 2,\n          q: 3,\n          tubularSegments: 128,\n          radialSegments: 24,\n          material: {\n            color: \"#ffffff\",\n            metalness: 1,\n            roughness: 0,\n          },\n        },\n        children: [],\n      },\n      accent1: {\n        type: \"Float\",\n        props: {\n          position: [-1.5, 1.8, 0.5],\n          rotation: null,\n          scale: null,\n          speed: 2,\n          rotationIntensity: 0.5,\n          floatIntensity: 1.2,\n          enabled: true,\n        },\n        children: [\"accent1-body\"],\n      },\n      \"accent1-body\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.15,\n          widthSegments: null,\n          heightSegments: null,\n          color: \"#eeeeff\",\n          transmission: 1,\n          thickness: 0.3,\n          roughness: 0,\n          chromaticAberration: 0.06,\n          ior: 1.5,\n          distortion: null,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      accent2: {\n        type: \"Float\",\n        props: {\n          position: [1.2, 2.5, -0.8],\n          rotation: null,\n          scale: null,\n          speed: 1.8,\n          rotationIntensity: 0.4,\n          floatIntensity: 1.5,\n          enabled: true,\n        },\n        children: [\"accent2-body\"],\n      },\n      \"accent2-body\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.1,\n          widthSegments: null,\n          heightSegments: null,\n          color: \"#ffeeff\",\n          transmission: 1,\n          thickness: 0.2,\n          roughness: 0,\n          chromaticAberration: 0.08,\n          ior: 1.6,\n          distortion: null,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      accent3: {\n        type: \"Float\",\n        props: {\n          position: [0.8, 1.5, 1.2],\n          rotation: null,\n          scale: null,\n          speed: 2.2,\n          rotationIntensity: 0.6,\n          floatIntensity: 0.8,\n          enabled: true,\n        },\n        children: [\"accent3-body\"],\n      },\n      \"accent3-body\": {\n        type: \"GlassSphere\",\n        props: {\n          position: null,\n          rotation: null,\n          scale: null,\n          castShadow: null,\n          receiveShadow: null,\n          radius: 0.12,\n          widthSegments: null,\n          heightSegments: null,\n          color: \"#eeffee\",\n          transmission: 1,\n          thickness: 0.25,\n          roughness: 0,\n          chromaticAberration: 0.05,\n          ior: 1.4,\n          distortion: null,\n          distortionScale: null,\n          temporalDistortion: null,\n          samples: 6,\n          resolution: 128,\n        },\n        children: [],\n      },\n      floor: {\n        type: \"ReflectorPlane\",\n        props: {\n          position: [0, -0.5, 0],\n          rotation: [-1.5708, 0, 0],\n          scale: null,\n          width: 25,\n          height: 25,\n          color: \"#0a0a0a\",\n          resolution: 1024,\n          blur: 300,\n          mirror: 0.6,\n          mixBlur: 8,\n          mixStrength: 2,\n          depthScale: 0.2,\n          metalness: 0.8,\n          roughness: 0.3,\n        },\n        children: [],\n      },\n      shadows: {\n        type: \"ContactShadows\",\n        props: {\n          position: [0, -0.49, 0],\n          rotation: null,\n          scale: null,\n          opacity: 0.5,\n          width: 10,\n          height: 10,\n          blur: 2,\n          near: null,\n          far: 5,\n          smooth: true,\n          resolution: 512,\n          frames: null,\n          color: \"#000000\",\n        },\n        children: [],\n      },\n      sparkles: {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 2, 0],\n          rotation: null,\n          scale: [6, 4, 6],\n          count: 30,\n          speed: 0.05,\n          opacity: 0.15,\n          color: \"#ffffff\",\n          size: 0.3,\n          noise: 0.5,\n        },\n        children: [],\n      },\n      fx: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 0.5,\n          luminanceThreshold: 0.3,\n          luminanceSmoothing: 0.4,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.3, darkness: 0.5 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: true,\n          dampingFactor: null,\n          enableZoom: null,\n          enablePan: null,\n          enableRotate: null,\n          minDistance: 3,\n          maxDistance: 12,\n          minPolarAngle: 0.3,\n          maxPolarAngle: 1.4,\n          autoRotate: true,\n          autoRotateSpeed: 1,\n          target: [0, 1.5, 0],\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/starfield.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const starfield: Scene = {\n  name: \"Starfield\",\n  description:\n    \"Classic Windows starfield screensaver -- flying through stars at warp speed\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"cam\",\n          \"ambient\",\n          \"tunnel\",\n          \"stars1\",\n          \"stars2\",\n          \"sparkle-dust\",\n          \"shake\",\n          \"post\",\n          \"controls\",\n        ],\n      },\n      cam: {\n        type: \"PerspectiveCamera\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          fov: 75,\n          near: 0.1,\n          far: 1000,\n          makeDefault: true,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#111122\", intensity: 0.15 },\n        children: [],\n      },\n      tunnel: {\n        type: \"WarpTunnel\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          ringCount: 120,\n          radius: 4,\n          length: 40,\n          speed: 12,\n          tubeRadius: 0.008,\n          color1: \"#ffffff\",\n          color2: \"#aabbff\",\n        },\n        children: [],\n      },\n      stars1: {\n        type: \"Stars\",\n        props: {\n          radius: 50,\n          depth: 80,\n          count: 10000,\n          factor: 6,\n          saturation: 0,\n          fade: true,\n          speed: 3,\n        },\n        children: [],\n      },\n      stars2: {\n        type: \"Stars\",\n        props: {\n          radius: 30,\n          depth: 50,\n          count: 5000,\n          factor: 3,\n          saturation: 0.1,\n          fade: true,\n          speed: 5,\n        },\n        children: [],\n      },\n      \"sparkle-dust\": {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 0, -5],\n          rotation: null,\n          scale: [8, 8, 30],\n          count: 300,\n          speed: 2,\n          opacity: 0.6,\n          color: \"#ffffff\",\n          size: 1,\n          noise: 4,\n        },\n        children: [],\n      },\n      shake: {\n        type: \"CameraShake\",\n        props: {\n          intensity: 0.15,\n          maxYaw: 0.02,\n          maxPitch: 0.02,\n          maxRoll: 0.01,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 1.5,\n          luminanceThreshold: 0.15,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.1, darkness: 0.9 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: null,\n          dampingFactor: null,\n          enableZoom: false,\n          enablePan: false,\n          enableRotate: false,\n          minDistance: null,\n          maxDistance: null,\n          minPolarAngle: null,\n          maxPolarAngle: null,\n          autoRotate: false,\n          autoRotateSpeed: null,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/app/scenes/storm-cell.ts",
    "content": "import type { Scene } from \"./_helpers\";\n\nexport const stormCell: Scene = {\n  name: \"Storm Cell\",\n  description:\n    \"Dark violent thunderstorm -- spinning cloud masses, pulsing lightning, rain particles, camera shake\",\n  spec: {\n    root: \"scene\",\n    elements: {\n      scene: {\n        type: \"Group\",\n        props: { position: null, rotation: null, scale: null },\n        children: [\n          \"sky\",\n          \"ambient\",\n          \"sun\",\n          \"cloud-spin-1\",\n          \"cloud-spin-2\",\n          \"cloud-spin-3\",\n          \"lightning-1-pulse\",\n          \"lightning-2-pulse\",\n          \"lightning-3-pulse\",\n          \"rain\",\n          \"shake\",\n          \"post\",\n          \"fog\",\n          \"controls\",\n        ],\n      },\n      sky: {\n        type: \"Sky\",\n        props: {\n          distance: null,\n          sunPosition: [10, 2, 0],\n          inclination: null,\n          azimuth: null,\n          turbidity: 20,\n          rayleigh: 0.5,\n          mieCoefficient: 0.01,\n          mieDirectionalG: 0.9,\n        },\n        children: [],\n      },\n      ambient: {\n        type: \"AmbientLight\",\n        props: { color: \"#111122\", intensity: 0.15 },\n        children: [],\n      },\n      sun: {\n        type: \"DirectionalLight\",\n        props: {\n          position: [10, 3, 0],\n          rotation: null,\n          scale: null,\n          color: \"#334466\",\n          intensity: 0.5,\n          castShadow: true,\n        },\n        children: [],\n      },\n      \"cloud-spin-1\": {\n        type: \"Spin\",\n        props: {\n          position: [0, 4, 0],\n          rotation: null,\n          scale: null,\n          speed: 0.08,\n          axis: \"y\",\n        },\n        children: [\"cloud-1\"],\n      },\n      \"cloud-1\": {\n        type: \"Cloud\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          seed: 1,\n          segments: 40,\n          bounds: [25, 3, 25],\n          volume: 15,\n          speed: 0.3,\n          fade: 20,\n          opacity: 0.9,\n          color: \"#222233\",\n          growth: 10,\n        },\n        children: [],\n      },\n      \"cloud-spin-2\": {\n        type: \"Spin\",\n        props: {\n          position: [0, 6, -3],\n          rotation: null,\n          scale: null,\n          speed: -0.05,\n          axis: \"y\",\n        },\n        children: [\"cloud-2\"],\n      },\n      \"cloud-2\": {\n        type: \"Cloud\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          seed: 42,\n          segments: 30,\n          bounds: [20, 2, 20],\n          volume: 12,\n          speed: 0.2,\n          fade: 15,\n          opacity: 0.7,\n          color: \"#1a1a2a\",\n          growth: 8,\n        },\n        children: [],\n      },\n      \"cloud-spin-3\": {\n        type: \"Spin\",\n        props: {\n          position: [0, 8, 2],\n          rotation: null,\n          scale: null,\n          speed: 0.03,\n          axis: \"y\",\n        },\n        children: [\"cloud-3\"],\n      },\n      \"cloud-3\": {\n        type: \"Cloud\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          seed: 99,\n          segments: 25,\n          bounds: [30, 2, 30],\n          volume: 10,\n          speed: 0.15,\n          fade: 25,\n          opacity: 0.5,\n          color: \"#2a2a3a\",\n          growth: 6,\n        },\n        children: [],\n      },\n      \"lightning-1-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [2, 5, -1],\n          rotation: null,\n          scale: null,\n          speed: 3,\n          min: 0,\n          max: 1,\n        },\n        children: [\"lightning-1\"],\n      },\n      \"lightning-1\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#aabbff\",\n          intensity: 80,\n          distance: 30,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"lightning-2-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [-3, 6, 3],\n          rotation: null,\n          scale: null,\n          speed: 4.5,\n          min: 0,\n          max: 1,\n        },\n        children: [\"lightning-2\"],\n      },\n      \"lightning-2\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#aaccff\",\n          intensity: 60,\n          distance: 25,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      \"lightning-3-pulse\": {\n        type: \"Pulse\",\n        props: {\n          position: [0, 4, -4],\n          rotation: null,\n          scale: null,\n          speed: 2.5,\n          min: 0,\n          max: 1,\n        },\n        children: [\"lightning-3\"],\n      },\n      \"lightning-3\": {\n        type: \"PointLight\",\n        props: {\n          position: [0, 0, 0],\n          rotation: null,\n          scale: null,\n          color: \"#99bbff\",\n          intensity: 40,\n          distance: 20,\n          decay: 2,\n          castShadow: false,\n        },\n        children: [],\n      },\n      rain: {\n        type: \"Sparkles\",\n        props: {\n          position: [0, 5, 0],\n          rotation: null,\n          scale: [20, 15, 20],\n          count: 500,\n          speed: 5,\n          opacity: 0.4,\n          color: \"#6688aa\",\n          size: 0.5,\n          noise: 0.5,\n        },\n        children: [],\n      },\n      shake: {\n        type: \"CameraShake\",\n        props: {\n          intensity: 0.2,\n          maxYaw: 0.02,\n          maxPitch: 0.03,\n          maxRoll: 0.01,\n        },\n        children: [],\n      },\n      post: {\n        type: \"EffectComposer\",\n        props: { enabled: true, multisampling: 8 },\n        children: [\"bloom\", \"vignette\"],\n      },\n      bloom: {\n        type: \"Bloom\",\n        props: {\n          intensity: 0.8,\n          luminanceThreshold: 0.3,\n          luminanceSmoothing: null,\n          mipmapBlur: true,\n        },\n        children: [],\n      },\n      vignette: {\n        type: \"Vignette\",\n        props: { offset: 0.3, darkness: 0.8 },\n        children: [],\n      },\n      fog: {\n        type: \"Fog\",\n        props: { color: \"#0a0a15\", near: 5, far: 30 },\n        children: [],\n      },\n      controls: {\n        type: \"OrbitControls\",\n        props: {\n          enableDamping: null,\n          dampingFactor: null,\n          enableZoom: true,\n          enablePan: null,\n          enableRotate: true,\n          minDistance: 3,\n          maxDistance: 20,\n          minPolarAngle: 0.2,\n          maxPolarAngle: 1.3,\n          autoRotate: true,\n          autoRotateSpeed: 0.1,\n        },\n        children: [],\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/react-three-fiber/eslint.config.js",
    "content": "import { nextJsConfig } from \"@internal/eslint-config/next-js\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...nextJsConfig,\n  {\n    rules: {\n      \"react/prop-types\": \"off\",\n      \"react/no-unknown-property\": \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "examples/react-three-fiber/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/react-three-fiber/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  transpilePackages: [\n    \"@json-render/core\",\n    \"@json-render/react\",\n    \"@json-render/react-three-fiber\",\n    \"three\",\n  ],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/react-three-fiber/package.json",
    "content": "{\n  \"name\": \"example-react-three-fiber\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless react-three-fiber-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint --max-warnings 0\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/react-three-fiber\": \"workspace:*\",\n    \"@react-three/drei\": \"^10.7.7\",\n    \"@react-three/fiber\": \"^9.5.0\",\n    \"next\": \"16.1.6\",\n    \"react\": \"19.2.4\",\n    \"react-dom\": \"19.2.4\",\n    \"three\": \"^0.183.2\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"@types/three\": \"^0.183.1\",\n    \"eslint\": \"^9.39.1\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/react-three-fiber/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/nextjs.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"declaration\": false,\n    \"declarationMap\": false,\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/remotion/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Override the default model used for video generation\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/remotion/CHANGELOG.md",
    "content": "# example-remotion\n\n## 0.1.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/remotion@0.14.1\n\n## 0.1.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/remotion@0.14.0\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/remotion@0.13.0\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/remotion@0.12.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/remotion@0.12.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/remotion@0.11.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/remotion@0.10.0\n\n## 0.1.2\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n- @json-render/remotion@0.9.1\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/remotion@0.9.0\n"
  },
  {
    "path": "examples/remotion/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { getVideoPrompt } from \"@/lib/catalog\";\nimport { minuteRateLimit, dailyRateLimit } from \"@/lib/rate-limit\";\nimport { headers } from \"next/headers\";\n\nexport const maxDuration = 30;\n\n// Generate prompt from catalog - uses Remotion schema's prompt template with custom rules\nconst SYSTEM_PROMPT = getVideoPrompt();\n\nconst MAX_PROMPT_LENGTH = 500;\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nexport async function POST(req: Request) {\n  const headersList = await headers();\n  const ip = headersList.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const { prompt } = await req.json();\n\n  const sanitizedPrompt = String(prompt || \"\").slice(0, MAX_PROMPT_LENGTH);\n\n  const result = streamText({\n    model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,\n    system: SYSTEM_PROMPT,\n    prompt: sanitizedPrompt,\n    temperature: 0.7,\n  });\n\n  return result.toTextStreamResponse();\n}\n"
  },
  {
    "path": "examples/remotion/app/globals.css",
    "content": "@import \"tailwindcss\";\n\n@theme {\n  --color-background: oklch(1 0 0);\n  --color-foreground: oklch(0.145 0 0);\n  --color-muted: oklch(0.97 0 0);\n  --color-muted-foreground: oklch(0.556 0 0);\n  --color-border: oklch(0.922 0 0);\n  --color-primary: oklch(0.205 0 0);\n  --color-primary-foreground: oklch(0.985 0 0);\n  --radius: 0.625rem;\n}\n\n@media (prefers-color-scheme: dark) {\n  @theme {\n    --color-background: oklch(0.145 0 0);\n    --color-foreground: oklch(0.985 0 0);\n    --color-muted: oklch(0.269 0 0);\n    --color-muted-foreground: oklch(0.708 0 0);\n    --color-border: oklch(0.269 0 0);\n    --color-primary: oklch(0.922 0 0);\n    --color-primary-foreground: oklch(0.205 0 0);\n  }\n}\n\n* {\n  border-color: var(--color-border);\n}\n\nbody {\n  background: var(--color-background);\n  color: var(--color-foreground);\n  font-family: system-ui, sans-serif;\n}\n"
  },
  {
    "path": "examples/remotion/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport \"./globals.css\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n  weight: \"100 900\",\n});\n\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n  weight: \"100 900\",\n});\n\nexport const metadata: Metadata = {\n  title: \"json-render/remotion\",\n  description: \"AI to Video with json-render\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body\n        className={`${geistSans.variable} ${geistMono.variable} antialiased`}\n      >\n        {children}\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/remotion/app/page.tsx",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { createSpecStreamCompiler } from \"@json-render/core\";\nimport { Player, PlayerRef } from \"@remotion/player\";\nimport { Renderer, type TimelineSpec } from \"@json-render/remotion\";\nimport { createHighlighter, type Highlighter } from \"shiki\";\n\n/**\n * Check if spec is complete enough to render\n */\nfunction isSpecComplete(spec: TimelineSpec): spec is Required<TimelineSpec> {\n  return !!(\n    spec.composition &&\n    spec.tracks &&\n    Array.isArray(spec.clips) &&\n    spec.clips.length > 0\n  );\n}\n\n/**\n * Shiki theme (Vercel-inspired dark theme)\n */\nconst darkTheme = {\n  name: \"custom-dark\",\n  type: \"dark\" as const,\n  colors: {\n    \"editor.background\": \"transparent\",\n    \"editor.foreground\": \"#EDEDED\",\n  },\n  settings: [\n    {\n      scope: [\"string\", \"string.quoted\"],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\n        \"constant.numeric\",\n        \"constant.language.boolean\",\n        \"constant.language.null\",\n      ],\n      settings: { foreground: \"#50E3C2\" },\n    },\n    {\n      scope: [\"punctuation\", \"meta.brace\", \"meta.bracket\"],\n      settings: { foreground: \"#888888\" },\n    },\n    {\n      scope: [\"support.type.property-name\", \"entity.name.tag.json\"],\n      settings: { foreground: \"#EDEDED\" },\n    },\n  ],\n};\n\n// Preload highlighter\nlet highlighterPromise: Promise<Highlighter> | null = null;\n\nfunction getHighlighter() {\n  if (!highlighterPromise) {\n    highlighterPromise = createHighlighter({\n      themes: [darkTheme],\n      langs: [\"json\"],\n    });\n  }\n  return highlighterPromise;\n}\n\n// Start loading immediately\nif (typeof window !== \"undefined\") {\n  getHighlighter();\n}\n\n/**\n * Code block with syntax highlighting\n */\nfunction CodeBlock({ code }: { code: string }) {\n  const [html, setHtml] = useState<string>(\"\");\n\n  useEffect(() => {\n    getHighlighter().then((highlighter) => {\n      setHtml(\n        highlighter.codeToHtml(code, {\n          lang: \"json\",\n          theme: \"custom-dark\",\n        }),\n      );\n    });\n  }, [code]);\n\n  if (!html) {\n    return (\n      <pre className=\"p-4 text-left\">\n        <code className=\"text-muted-foreground\">{code}</code>\n      </pre>\n    );\n  }\n\n  return (\n    <div\n      className=\"p-4 text-[13px] leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-0! [&_pre]:m-0! [&_code]:bg-transparent!\"\n      dangerouslySetInnerHTML={{ __html: html }}\n    />\n  );\n}\n\n/**\n * Copy button component\n */\nfunction CopyButton({ text }: { text: string }) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async () => {\n    await navigator.clipboard.writeText(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <button\n      onClick={handleCopy}\n      className=\"p-1.5 rounded hover:bg-white/10 transition-colors text-muted-foreground hover:text-foreground\"\n      aria-label=\"Copy JSON\"\n    >\n      {copied ? (\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <polyline points=\"20 6 9 17 4 12\" />\n        </svg>\n      ) : (\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n          <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n        </svg>\n      )}\n    </button>\n  );\n}\n\nconst EXAMPLE_PROMPTS = [\n  \"Create a 10-second product intro with title cards and images\",\n  \"Make a photo slideshow with 5 different images\",\n  \"Build a testimonial video with quotes and background images\",\n  \"Design a dynamic company intro with animated entrances\",\n];\n\nexport default function Home() {\n  const [prompt, setPrompt] = useState(\"\");\n  const [isGenerating, setIsGenerating] = useState(false);\n  const [spec, setSpec] = useState<TimelineSpec | null>(null);\n  const [error, setError] = useState<string | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const playerRef = useRef<PlayerRef>(null);\n\n  const generate = useCallback(async () => {\n    if (!prompt.trim() || isGenerating) return;\n\n    setIsGenerating(true);\n    setError(null);\n    setSpec(null);\n\n    try {\n      const response = await fetch(\"/api/generate\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt }),\n      });\n\n      if (!response.ok) {\n        throw new Error(\"Generation failed\");\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n      const compiler = createSpecStreamCompiler<TimelineSpec>();\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        const chunk = decoder.decode(value, { stream: true });\n        const { result, newPatches } = compiler.push(chunk);\n\n        if (newPatches.length > 0) {\n          setSpec(result);\n        }\n      }\n\n      // Get final result (processes any remaining buffer)\n      const finalSpec = compiler.getResult();\n      setSpec(finalSpec);\n\n      // Final validation and auto-play\n      if (isSpecComplete(finalSpec)) {\n        // Auto-play after a short delay to ensure Player is mounted\n        setTimeout(() => {\n          playerRef.current?.play();\n        }, 100);\n      } else {\n        setError(\"Generated timeline is incomplete\");\n      }\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"Generation failed\");\n    } finally {\n      setIsGenerating(false);\n    }\n  }, [prompt, isGenerating]);\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    generate();\n  };\n\n  const handleExampleClick = (examplePrompt: string) => {\n    setPrompt(examplePrompt);\n    setTimeout(() => inputRef.current?.focus(), 0);\n  };\n\n  const handleExport = useCallback(() => {\n    if (!spec) return;\n    const blob = new Blob([JSON.stringify(spec, null, 2)], {\n      type: \"application/json\",\n    });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = \"timeline.json\";\n    a.click();\n    URL.revokeObjectURL(url);\n  }, [spec]);\n\n  return (\n    <div className=\"min-h-screen\">\n      {/* Hero */}\n      <section className=\"max-w-5xl mx-auto px-6 pt-24 pb-16 text-center\">\n        <div className=\"text-xs font-mono text-muted-foreground mb-4\">\n          @json-render/remotion\n        </div>\n        <h1 className=\"text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6\">\n          AI &rarr; json-render &rarr; Video\n        </h1>\n        <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed\">\n          Define a video catalog. Users prompt. AI outputs timeline JSON\n          constrained to your components. Remotion renders it.\n        </p>\n\n        {/* Demo */}\n        <div className=\"max-w-4xl mx-auto\">\n          {/* Prompt Input */}\n          <form onSubmit={handleSubmit} className=\"mb-4\">\n            <div className=\"border border-border rounded p-3 bg-background font-mono text-sm flex items-center gap-2\">\n              <span className=\"text-muted-foreground\">&gt;</span>\n              <input\n                ref={inputRef}\n                type=\"text\"\n                value={prompt}\n                onChange={(e) => setPrompt(e.target.value)}\n                placeholder=\"Describe the video you want to create...\"\n                className=\"flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50\"\n                disabled={isGenerating}\n                maxLength={500}\n              />\n              {isGenerating ? (\n                <button\n                  type=\"button\"\n                  onClick={() => setIsGenerating(false)}\n                  className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors\"\n                >\n                  <svg\n                    width=\"12\"\n                    height=\"12\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"currentColor\"\n                  >\n                    <rect x=\"6\" y=\"6\" width=\"12\" height=\"12\" />\n                  </svg>\n                </button>\n              ) : (\n                <button\n                  type=\"submit\"\n                  disabled={!prompt.trim()}\n                  className=\"w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30\"\n                >\n                  <svg\n                    width=\"14\"\n                    height=\"14\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"2\"\n                  >\n                    <path d=\"M5 12h14M12 5l7 7-7 7\" />\n                  </svg>\n                </button>\n              )}\n            </div>\n          </form>\n\n          {/* Example prompts */}\n          <div className=\"flex flex-wrap gap-2 justify-center mb-6\">\n            {EXAMPLE_PROMPTS.map((examplePrompt) => (\n              <button\n                key={examplePrompt}\n                onClick={() => handleExampleClick(examplePrompt)}\n                className=\"text-xs px-3 py-1.5 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors\"\n              >\n                {examplePrompt}\n              </button>\n            ))}\n          </div>\n\n          {error && (\n            <div className=\"mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded text-red-500 text-sm\">\n              {error}\n            </div>\n          )}\n\n          <div className=\"grid lg:grid-cols-2 gap-4\">\n            {/* JSON Panel */}\n            <div className=\"text-left\">\n              <div className=\"flex items-center gap-4 mb-2 h-6\">\n                <span className=\"text-xs font-mono text-muted-foreground\">\n                  json\n                </span>\n                {isGenerating && (\n                  <span className=\"text-xs text-muted-foreground animate-pulse\">\n                    generating...\n                  </span>\n                )}\n              </div>\n              <div className=\"border border-border rounded bg-background font-mono text-xs h-[28rem] overflow-auto relative group\">\n                {spec && (\n                  <div className=\"absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity\">\n                    <CopyButton text={JSON.stringify(spec, null, 2)} />\n                  </div>\n                )}\n                {spec ? (\n                  <CodeBlock code={JSON.stringify(spec, null, 2)} />\n                ) : isGenerating ? (\n                  <div className=\"p-4 text-muted-foreground/50 h-full flex items-center justify-center\">\n                    <div className=\"flex flex-col items-center gap-2\">\n                      <div className=\"flex gap-1\">\n                        <span className=\"w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce\" />\n                        <span className=\"w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:0.1s]\" />\n                        <span className=\"w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:0.2s]\" />\n                      </div>\n                      <span>Generating timeline...</span>\n                    </div>\n                  </div>\n                ) : (\n                  <div className=\"p-4 text-muted-foreground/50 h-full flex items-center justify-center\">\n                    Enter a prompt to generate a video timeline\n                  </div>\n                )}\n              </div>\n            </div>\n\n            {/* Video Preview Panel */}\n            <div>\n              <div className=\"flex items-center justify-between mb-2 h-6\">\n                <span className=\"text-xs font-mono text-muted-foreground\">\n                  preview\n                </span>\n                <div className=\"flex items-center gap-2\">\n                  {spec && isSpecComplete(spec) && (\n                    <button\n                      onClick={handleExport}\n                      className=\"text-xs px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors\"\n                      title=\"Export timeline JSON\"\n                    >\n                      Export\n                    </button>\n                  )}\n                </div>\n              </div>\n              <div\n                className=\"border border-border rounded bg-black h-[28rem] relative overflow-hidden\"\n                data-player-container\n              >\n                {spec && isSpecComplete(spec) ? (\n                  <Player\n                    ref={playerRef}\n                    component={Renderer}\n                    inputProps={{ spec }}\n                    durationInFrames={spec.composition.durationInFrames}\n                    fps={spec.composition.fps}\n                    compositionWidth={spec.composition.width}\n                    compositionHeight={spec.composition.height}\n                    style={{\n                      width: \"100%\",\n                      height: \"100%\",\n                    }}\n                    controls\n                    loop\n                  />\n                ) : (\n                  <div className=\"h-full flex items-center justify-center text-white/30 text-sm\">\n                    {isGenerating ? (\n                      <div className=\"flex flex-col items-center gap-2\">\n                        <div className=\"flex gap-1\">\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce\" />\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]\" />\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]\" />\n                        </div>\n                        <span>Generating timeline...</span>\n                      </div>\n                    ) : spec ? (\n                      <div className=\"flex flex-col items-center gap-2\">\n                        <div className=\"flex gap-1\">\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce\" />\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]\" />\n                          <span className=\"w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]\" />\n                        </div>\n                        <span>Building timeline...</span>\n                      </div>\n                    ) : (\n                      \"Enter a prompt to generate a video\"\n                    )}\n                  </div>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/remotion/lib/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport {\n  schema,\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n} from \"@json-render/remotion/server\";\n\n/**\n * Custom rules for the AI to follow when generating videos\n */\nconst customRules = [\n  // Image URLs using Picsum (free, no API key)\n  'ImageSlide props: { \"src\": \"https://picsum.photos/1920/1080?random=N\", \"alt\": \"description\" } - use \"src\" NOT \"imageUrl\"',\n  \"Use different random numbers for each image to get different photos (e.g., ?random=1, ?random=2, ?random=3)\",\n  \"Picsum provides random professional stock photos - great for product shots, backgrounds, and visual content\",\n];\n\n/**\n * Remotion video catalog\n *\n * Uses standard definitions from @json-render/remotion,\n * with the ability to add custom components.\n *\n * @example Adding a custom component:\n * ```ts\n * components: {\n *   ...standardComponentDefinitions,\n *   MyCustomComponent: {\n *     props: z.object({ ... }),\n *     type: \"scene\",\n *     defaultDuration: 90,\n *     description: \"My custom component\",\n *   },\n * },\n * ```\n */\nexport const videoCatalog = defineCatalog(schema, {\n  // Use all standard components from the package\n  components: standardComponentDefinitions,\n\n  // Use all standard transitions from the package\n  transitions: standardTransitionDefinitions,\n\n  // Use all standard effects from the package\n  effects: standardEffectDefinitions,\n});\n\n/**\n * Get the prompt with custom rules for image generation\n */\nexport function getVideoPrompt(): string {\n  return videoCatalog.prompt({ customRules });\n}\n"
  },
  {
    "path": "examples/remotion/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:remotion:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:remotion:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/remotion/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "examples/remotion/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/dev/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/remotion/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  transpilePackages: [\"@json-render/core\", \"@json-render/remotion\"],\n  turbopack: {\n    resolveAlias: {\n      // Deduplicate remotion — pnpm creates separate copies when peer\n      // dependency versions (React) differ between workspace packages.\n      // Force all imports to resolve from the app's node_modules.\n      remotion: \"./node_modules/remotion\",\n    },\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/remotion/package.json",
    "content": "{\n  \"name\": \"example-remotion\",\n  \"version\": \"0.1.9\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless remotion-demo.json-render next dev --turbopack\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.13\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/remotion\": \"workspace:*\",\n    \"@remotion/bundler\": \"4.0.418\",\n    \"@remotion/player\": \"4.0.418\",\n    \"@remotion/renderer\": \"4.0.418\",\n    \"ai\": \"^6.0.70\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"next\": \"16.1.1\",\n    \"next-themes\": \"^0.4.6\",\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\",\n    \"remotion\": \"4.0.418\",\n    \"shiki\": \"^3.21.0\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.1.18\",\n    \"@types/node\": \"^22.10.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"typescript\": \"^5.7.2\"\n  }\n}\n"
  },
  {
    "path": "examples/remotion/postcss.config.mjs",
    "content": "const config = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/remotion/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    },\n    \"target\": \"ES2017\"\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "examples/solid/CHANGELOG.md",
    "content": "# example-solid\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/solid@0.14.1\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/solid@0.14.0\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/solid@0.13.0\n"
  },
  {
    "path": "examples/solid/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>json-render solid example</title>\n    <meta property=\"og:title\" content=\"Solid Example | json-render\" />\n    <meta property=\"og:image\" content=\"/og-image.png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"Solid Example | json-render\" />\n    <meta name=\"twitter:image\" content=\"/og-image.png\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/solid/package.json",
    "content": "{\n  \"name\": \"example-solid\",\n  \"version\": \"0.1.3\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"portless solid-demo.json-render vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/solid\": \"workspace:*\",\n    \"solid-js\": \"^1.9.11\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\",\n    \"vite-plugin-solid\": \"^2.11.10\"\n  }\n}\n"
  },
  {
    "path": "examples/solid/src/App.tsx",
    "content": "import { StateProvider } from \"@json-render/solid\";\nimport { demoSpec } from \"./lib/spec\";\nimport { DemoRenderer } from \"./DemoRenderer\";\n\nexport function App() {\n  const initialState = demoSpec.state ?? {};\n\n  return (\n    <StateProvider initialState={initialState}>\n      <DemoRenderer />\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/DemoRenderer.tsx",
    "content": "import {\n  ActionProvider,\n  ValidationProvider,\n  VisibilityProvider,\n  Renderer,\n  useStateStore,\n} from \"@json-render/solid\";\nimport { demoSpec } from \"./lib/spec\";\nimport { registry } from \"./lib/registry\";\n\nexport function DemoRenderer() {\n  const stateStore = useStateStore();\n\n  const handlers = {\n    increment: async () => {\n      stateStore.set(\"/count\", Number(stateStore.get(\"/count\") || 0) + 1);\n    },\n    decrement: async () => {\n      stateStore.set(\n        \"/count\",\n        Math.max(0, Number(stateStore.get(\"/count\") || 0) - 1),\n      );\n    },\n    reset: async () => {\n      stateStore.set(\"/count\", 0);\n    },\n    toggleItem: async (params: Record<string, unknown>) => {\n      const index = params.index as number;\n      const todos = (\n        stateStore.get(\"/todos\") as Array<{\n          id: number;\n          title: string;\n          completed: boolean;\n        }>\n      ).map((item, i) =>\n        i === index ? { ...item, completed: !item.completed } : item,\n      );\n      stateStore.set(\"/todos\", todos);\n    },\n  };\n\n  return (\n    <ActionProvider handlers={handlers}>\n      <VisibilityProvider>\n        <ValidationProvider>\n          <Renderer spec={demoSpec} registry={registry} />\n        </ValidationProvider>\n      </VisibilityProvider>\n    </ActionProvider>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/app.css",
    "content": "* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n  background: #f9fafb;\n  color: #111827;\n  min-height: 100vh;\n}\n"
  },
  {
    "path": "examples/solid/src/index.tsx",
    "content": "import { render } from \"solid-js/web\";\nimport { App } from \"./App\";\nimport \"./app.css\";\n\nrender(() => <App />, document.getElementById(\"app\")!);\n"
  },
  {
    "path": "examples/solid/src/lib/catalog.ts",
    "content": "import { schema } from \"@json-render/solid/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = schema.createCatalog({\n  components: {\n    Stack: {\n      props: z.object({\n        gap: z.number().optional(),\n        padding: z.number().optional(),\n        direction: z.enum([\"vertical\", \"horizontal\"]).optional(),\n        align: z.enum([\"start\", \"center\", \"end\"]).optional(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Layout container that stacks children vertically or horizontally\",\n    },\n    Card: {\n      props: z.object({\n        title: z.string().optional(),\n        subtitle: z.string().optional(),\n      }),\n      slots: [\"default\"],\n      description: \"A card container with optional title and subtitle\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).optional(),\n        weight: z.enum([\"normal\", \"medium\", \"bold\"]).optional(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"Displays a text string\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).optional(),\n        disabled: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A clickable button that emits a 'press' event\",\n    },\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A small badge/tag label\",\n    },\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().optional(),\n        completed: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A single item in a list\",\n    },\n    Input: {\n      props: z.object({\n        value: z.string().optional(),\n        placeholder: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A text input field that supports two-way state binding\",\n    },\n  },\n  actions: {\n    increment: {\n      params: z.object({}),\n      description: \"Increment the counter by 1\",\n    },\n    decrement: {\n      params: z.object({}),\n      description: \"Decrement the counter by 1\",\n    },\n    reset: {\n      params: z.object({}),\n      description: \"Reset the counter to 0\",\n    },\n    toggleItem: {\n      params: z.object({\n        index: z.number(),\n      }),\n      description: \"Toggle the completed state of a todo item\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/solid/src/lib/components/Badge.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface BadgeProps {\n  label: string;\n  color?: string;\n}\n\nexport function Badge(renderProps: BaseComponentProps<BadgeProps>) {\n  const color = () => renderProps.props.color ?? \"#6b7280\";\n\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        padding: \"4px 12px\",\n        \"border-radius\": \"9999px\",\n        \"font-size\": \"13px\",\n        \"font-weight\": \"500\",\n        \"background-color\": `${color()}20`,\n        color: color(),\n        border: `1px solid ${color()}40`,\n      }}\n    >\n      {renderProps.props.label}\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/Button.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface ButtonProps {\n  label: string;\n  variant?: \"primary\" | \"secondary\" | \"danger\";\n  disabled?: boolean;\n}\n\nconst variantStyles: Record<string, Record<string, string>> = {\n  primary: {\n    background: \"#2563eb\",\n    color: \"#ffffff\",\n    border: \"1px solid #2563eb\",\n  },\n  secondary: {\n    background: \"#f3f4f6\",\n    color: \"#374151\",\n    border: \"1px solid #d1d5db\",\n  },\n  danger: {\n    background: \"#fee2e2\",\n    color: \"#dc2626\",\n    border: \"1px solid #fca5a5\",\n  },\n};\n\nexport function Button(renderProps: BaseComponentProps<ButtonProps>) {\n  const variant = () => renderProps.props.variant ?? \"primary\";\n\n  return (\n    <button\n      disabled={renderProps.props.disabled}\n      onClick={() => renderProps.emit(\"press\")}\n      style={{\n        padding: \"8px 16px\",\n        \"border-radius\": \"8px\",\n        \"font-size\": \"14px\",\n        \"font-weight\": \"500\",\n        cursor: renderProps.props.disabled ? \"not-allowed\" : \"pointer\",\n        opacity: renderProps.props.disabled ? \"0.5\" : \"1\",\n        ...variantStyles[variant()],\n      }}\n    >\n      {renderProps.props.label}\n    </button>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/Card.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface CardProps {\n  title?: string;\n  subtitle?: string;\n}\n\nexport function Card(renderProps: BaseComponentProps<CardProps>) {\n  return (\n    <div\n      style={{\n        background: \"#ffffff\",\n        \"border-radius\": \"12px\",\n        border: \"1px solid #e5e7eb\",\n        padding: \"20px\",\n        \"box-shadow\": \"0 1px 3px rgba(0,0,0,0.06)\",\n      }}\n    >\n      {renderProps.props.title && (\n        <h2\n          style={{\n            \"font-size\": \"18px\",\n            \"font-weight\": \"600\",\n            \"margin-bottom\": renderProps.props.subtitle ? \"4px\" : \"16px\",\n          }}\n        >\n          {renderProps.props.title}\n        </h2>\n      )}\n      {renderProps.props.subtitle && (\n        <p\n          style={{\n            \"font-size\": \"14px\",\n            color: \"#6b7280\",\n            \"margin-bottom\": \"16px\",\n          }}\n        >\n          {renderProps.props.subtitle}\n        </p>\n      )}\n      {renderProps.children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/Input.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\nimport { useBoundProp } from \"@json-render/solid\";\n\ninterface InputProps {\n  value?: string;\n  placeholder?: string;\n}\n\nexport function Input(renderProps: BaseComponentProps<InputProps>) {\n  const [value, setValue] = useBoundProp<string>(\n    renderProps.props.value,\n    renderProps.bindings?.value,\n  );\n\n  return (\n    <input\n      value={String(value ?? \"\")}\n      onInput={(e) => setValue(e.currentTarget.value)}\n      placeholder={renderProps.props.placeholder ?? \"\"}\n      style={{\n        padding: \"8px 12px\",\n        \"border-radius\": \"8px\",\n        border: \"1px solid #d1d5db\",\n        \"font-size\": \"14px\",\n        outline: \"none\",\n        width: \"100%\",\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/ListItem.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface ListItemProps {\n  title: string;\n  description?: string;\n  completed?: boolean;\n}\n\nexport function ListItem(renderProps: BaseComponentProps<ListItemProps>) {\n  return (\n    <div\n      onClick={() => renderProps.emit(\"press\")}\n      style={{\n        display: \"flex\",\n        \"align-items\": \"center\",\n        gap: \"10px\",\n        padding: \"10px 12px\",\n        \"border-radius\": \"8px\",\n        border: \"1px solid #e5e7eb\",\n        cursor: \"pointer\",\n        \"user-select\": \"none\",\n        background: renderProps.props.completed ? \"#f0fdf4\" : \"#ffffff\",\n      }}\n    >\n      <span\n        style={{\n          width: \"22px\",\n          height: \"22px\",\n          \"border-radius\": \"6px\",\n          border: renderProps.props.completed\n            ? \"2px solid #22c55e\"\n            : \"2px solid #d1d5db\",\n          display: \"flex\",\n          \"align-items\": \"center\",\n          \"justify-content\": \"center\",\n          \"font-size\": \"13px\",\n          color: \"#22c55e\",\n          \"flex-shrink\": \"0\",\n        }}\n      >\n        {renderProps.props.completed ? \"\\u2713\" : \"\"}\n      </span>\n      <span\n        style={{\n          \"text-decoration\": renderProps.props.completed\n            ? \"line-through\"\n            : \"none\",\n          color: renderProps.props.completed ? \"#9ca3af\" : \"#111827\",\n        }}\n      >\n        {renderProps.props.title}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/Stack.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface StackProps {\n  gap?: number;\n  padding?: number;\n  direction?: \"vertical\" | \"horizontal\";\n  align?: \"start\" | \"center\" | \"end\";\n}\n\nexport function Stack(renderProps: BaseComponentProps<StackProps>) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        \"flex-direction\":\n          renderProps.props.direction === \"horizontal\" ? \"row\" : \"column\",\n        gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,\n        padding: renderProps.props.padding\n          ? `${renderProps.props.padding}px`\n          : undefined,\n        \"align-items\": renderProps.props.align,\n      }}\n    >\n      {renderProps.children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/components/Text.tsx",
    "content": "import type { BaseComponentProps } from \"@json-render/solid\";\n\ninterface TextProps {\n  content: string;\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n  weight?: \"normal\" | \"medium\" | \"bold\";\n  color?: string;\n}\n\nconst sizeMap: Record<string, string> = {\n  sm: \"12px\",\n  md: \"14px\",\n  lg: \"18px\",\n  xl: \"24px\",\n};\n\nconst weightMap: Record<string, string> = {\n  normal: \"400\",\n  medium: \"500\",\n  bold: \"700\",\n};\n\nexport function Text(renderProps: BaseComponentProps<TextProps>) {\n  return (\n    <span\n      style={{\n        \"font-size\": sizeMap[renderProps.props.size ?? \"md\"],\n        \"font-weight\": weightMap[renderProps.props.weight ?? \"normal\"],\n        color: renderProps.props.color,\n      }}\n    >\n      {String(renderProps.props.content ?? \"\")}\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/solid/src/lib/registry.tsx",
    "content": "import { type Components, defineRegistry } from \"@json-render/solid\";\nimport { catalog } from \"./catalog\";\nimport { Stack } from \"./components/Stack\";\nimport { Card } from \"./components/Card\";\nimport { Text } from \"./components/Text\";\nimport { Button } from \"./components/Button\";\nimport { Badge } from \"./components/Badge\";\nimport { ListItem } from \"./components/ListItem\";\nimport { Input } from \"./components/Input\";\n\nconst components: Components<typeof catalog> = {\n  Stack,\n  Card,\n  Text,\n  Button,\n  Badge,\n  ListItem,\n  Input,\n};\n\nexport const { registry, handlers: makeHandlers } = defineRegistry(catalog, {\n  components,\n  actions: {\n    increment: async () => {},\n    decrement: async () => {},\n    reset: async () => {},\n    toggleItem: async () => {},\n  },\n});\n"
  },
  {
    "path": "examples/solid/src/lib/spec.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport const demoSpec: Spec = {\n  root: \"root\",\n  state: {\n    count: 0,\n    name: \"\",\n    todos: [\n      { id: 1, title: \"Learn SolidJS\", completed: true },\n      { id: 2, title: \"Try @json-render/solid\", completed: false },\n      { id: 3, title: \"Build something awesome\", completed: false },\n    ],\n  },\n  elements: {\n    root: {\n      type: \"Stack\",\n      props: { gap: 24, padding: 24, direction: \"vertical\" },\n      children: [\n        \"header\",\n        \"counter-card\",\n        \"milestone-badge\",\n        \"todos-card\",\n        \"input-card\",\n      ],\n    },\n    header: {\n      type: \"Text\",\n      props: {\n        content: \"@json-render/solid demo\",\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"counter-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Counter\",\n        subtitle: \"Click the buttons to change the count\",\n      },\n      children: [\"counter-body\"],\n    },\n    \"counter-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"horizontal\", align: \"center\" },\n      children: [\n        \"decrement-btn\",\n        \"counter-value\",\n        \"increment-btn\",\n        \"reset-btn\",\n      ],\n    },\n    \"decrement-btn\": {\n      type: \"Button\",\n      props: { label: \"\\u2212\", variant: \"secondary\" },\n      on: { press: { action: \"decrement\" } },\n    },\n    \"counter-value\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/count\" },\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"increment-btn\": {\n      type: \"Button\",\n      props: { label: \"+\", variant: \"primary\" },\n      on: { press: { action: \"increment\" } },\n    },\n    \"reset-btn\": {\n      type: \"Button\",\n      props: { label: \"Reset\", variant: \"danger\" },\n      on: { press: { action: \"reset\" } },\n    },\n    \"milestone-badge\": {\n      type: \"Badge\",\n      props: { label: \"Milestone reached: 10!\", color: \"#10b981\" },\n      visible: { $state: \"/count\", gte: 10 },\n    },\n    \"todos-card\": {\n      type: \"Card\",\n      props: { title: \"Todo List\", subtitle: \"Your tasks\" },\n      children: [\"todos-list\"],\n    },\n    \"todos-list\": {\n      type: \"Stack\",\n      props: { gap: 8, direction: \"vertical\" },\n      repeat: { statePath: \"/todos\", key: \"id\" },\n      children: [\"todo-item\"],\n    },\n    \"todo-item\": {\n      type: \"ListItem\",\n      props: {\n        title: { $item: \"title\" },\n        completed: { $item: \"completed\" },\n      },\n      on: {\n        press: { action: \"toggleItem\", params: { index: { $index: true } } },\n      },\n    },\n    \"input-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Bound Input\",\n        subtitle: \"Type to update state and see reactive text\",\n      },\n      children: [\"input-body\"],\n    },\n    \"input-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"vertical\" },\n      children: [\"name-input\", \"name-display\"],\n    },\n    \"name-input\": {\n      type: \"Input\",\n      props: {\n        value: { $bindState: \"/name\" },\n        placeholder: \"Enter your name...\",\n      },\n    },\n    \"name-display\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/name\" },\n        size: \"md\",\n        color: \"#6b7280\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/solid/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\"\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/solid/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport solid from \"vite-plugin-solid\";\n\nexport default defineConfig({\n  plugins: [solid()],\n});\n"
  },
  {
    "path": "examples/stripe-app/README.md",
    "content": "# Stripe App Examples\n\n[Stripe Apps](https://stripe.com/docs/stripe-apps) examples demonstrating how to use json-render to build dynamic, AI-generated UI within the Stripe Dashboard.\n\n## Structure\n\n| Folder | Description |\n|--------|-------------|\n| [api/](./api) | Next.js server providing the `/api/generate` endpoint for AI-powered UI generation |\n| [drawer-app/](./drawer-app) | Standard Stripe App that renders in the Dashboard drawer (sidebar) |\n| [fullpage-app/](./fullpage-app) | Full-page Stripe App using `FullPageView` (requires alpha access) |\n\n## Quick Start\n\n### 1. Start the API server\n\n```bash\ncd api\npnpm install\ncp .env.example .env  # Set AI_GATEWAY_API_KEY\npnpm dev\n```\n\n### 2. Start a Stripe App\n\nFor the standard drawer app:\n\n```bash\ncd drawer-app\npnpm install\npnpm setup\nstripe apps start\n```\n\nFor the full-page app (requires alpha access):\n\n```bash\ncd fullpage-app\npnpm install\ncp .env.example .env  # Set STRIPE_APP_ID\npnpm setup\nstripe apps start\n# Navigate to https://dashboard.stripe.com/test/app/<your-app-id>\n```\n\n## Notes\n\n- The AI generation feature requires the API server to be running. Without it, the apps fall back to locally generated specs using real Stripe data.\n- The `fullpage-app` uses Stripe's full-page apps **private developer preview**. You need Stripe to enable the feature flag for your app and account. See [fullpage-app/README.md](./fullpage-app/README.md) for details.\n"
  },
  {
    "path": "examples/stripe-app/api/.env.example",
    "content": "# AI Gateway API Key (required for AI generation)\n# Get your key from Vercel AI Gateway dashboard\nAI_GATEWAY_API_KEY=\n"
  },
  {
    "path": "examples/stripe-app/api/README.md",
    "content": "# Stripe App API\n\nA lightweight Next.js server that provides the `/api/generate` endpoint for AI-powered UI generation in the Stripe App examples.\n\n## Setup\n\n```bash\npnpm install\n\ncp .env.example .env\n# Set AI_GATEWAY_API_KEY\n```\n\n## Running\n\n```bash\npnpm dev\n```\n\nRuns on port 3001. The Stripe apps (`drawer-app/` and `fullpage-app/`) expect this server to be running for AI generation features.\n\n## API\n\n### POST /api/generate\n\nAccepts `{ prompt, systemPrompt }` and returns a streamed text response with a json-render spec.\n"
  },
  {
    "path": "examples/stripe-app/api/app/api/generate/route.ts",
    "content": "import { streamText } from \"ai\";\nimport { gateway } from \"@ai-sdk/gateway\";\n\nexport const maxDuration = 60;\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nconst CORS_HEADERS: Record<string, string> = {\n  \"Access-Control-Allow-Origin\": \"*\",\n  \"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n  \"Access-Control-Allow-Headers\": \"Content-Type\",\n};\n\nexport async function POST(req: Request) {\n  const { prompt, systemPrompt } = await req.json();\n\n  if (!prompt) {\n    return new Response(JSON.stringify({ error: \"prompt is required\" }), {\n      status: 400,\n      headers: { \"Content-Type\": \"application/json\", ...CORS_HEADERS },\n    });\n  }\n\n  try {\n    const result = streamText({\n      model: gateway(DEFAULT_MODEL),\n      system: systemPrompt ?? \"You are a helpful UI builder.\",\n      prompt,\n      temperature: 0.7,\n    });\n\n    const response = result.toTextStreamResponse();\n\n    for (const [key, value] of Object.entries(CORS_HEADERS)) {\n      response.headers.set(key, value);\n    }\n\n    return response;\n  } catch (err) {\n    const message = err instanceof Error ? err.message : \"Generation failed\";\n    return new Response(JSON.stringify({ error: message }), {\n      status: 500,\n      headers: { \"Content-Type\": \"application/json\", ...CORS_HEADERS },\n    });\n  }\n}\n\nexport async function OPTIONS() {\n  return new Response(null, { headers: CORS_HEADERS });\n}\n"
  },
  {
    "path": "examples/stripe-app/api/app/layout.tsx",
    "content": "export const metadata = {\n  title: \"Stripe App API\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/stripe-app/api/app/page.tsx",
    "content": "export default function Home() {\n  return (\n    <main style={{ padding: \"2rem\", fontFamily: \"system-ui, sans-serif\" }}>\n      <h1>Stripe App API</h1>\n      <p>POST /api/generate to generate UI specs.</p>\n    </main>\n  );\n}\n"
  },
  {
    "path": "examples/stripe-app/api/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/stripe-app/api/next.config.ts",
    "content": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/stripe-app/api/package.json",
    "content": "{\n  \"name\": \"stripe-app-api\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless stripe-api-demo.json-render next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start --port 3001\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.50\",\n    \"ai\": \"^6.0.91\",\n    \"next\": \"^16.1.6\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.0.0\",\n    \"@types/react\": \"^19.1.0\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "examples/stripe-app/api/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/.env.example",
    "content": "# Stripe App ID (optional - overrides the template default)\n# Get this from your Stripe Apps dashboard after uploading\n# STRIPE_APP_ID=com.example.your-app-id\n\n# Stripe App Name (optional - overrides the template default)\n# STRIPE_APP_NAME=My App Name\n\n# AI Gateway API Key\n# Get your key from Vercel AI Gateway dashboard\nAI_GATEWAY_API_KEY=\n\n# Optional: Stripe API keys if you want to use real data\n# STRIPE_SECRET_KEY=sk_test_...\n# STRIPE_PUBLISHABLE_KEY=pk_test_...\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/CHANGELOG.md",
    "content": "# com.example.json-render-demo\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react@0.12.0\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/README.md",
    "content": "# Stripe App (Drawer)\n\nA [Stripe App](https://stripe.com/docs/stripe-apps) example demonstrating how to use json-render to build dynamic, AI-generated UI in the Stripe Dashboard drawer.\n\n## Views\n\n- **Home** - Revenue dashboard with AI-powered UI generation\n- **Customers** - Customer list and management\n- **Customer Details** - Individual customer view\n- **Payments** - Payment list and details\n- **Subscriptions** - Subscription management\n- **Invoices** - Invoice list\n- **Products** - Product catalog\n\n## Setup\n\n```bash\npnpm install\n\ncp .env.example .env\n# Optionally set STRIPE_APP_ID to your own app ID\n\npnpm setup\n```\n\n## Running\n\nStart the API server first (from `../api`):\n\n```bash\ncd ../api && pnpm dev\n```\n\nThen start the Stripe app:\n\n```bash\nstripe apps start\n```\n\n## How It Works\n\n1. **Component Catalog** - Maps json-render component types to Stripe UIXT components\n2. **Action Handlers** - Stripe operations (refunds, subscriptions, etc.)\n3. **StripeRenderer** - Custom renderer connecting json-render specs to Stripe components\n4. **Views** - Each view fetches data from the Stripe API and renders it using json-render specs\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/eslint.config.js",
    "content": "import { config as reactConfig } from \"@internal/eslint-config/react-internal\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...reactConfig,\n  {\n    rules: {\n      // Disable prop-types - we use TypeScript for type checking\n      \"react/prop-types\": \"off\",\n      // Allow underscore-prefixed unused variables\n      \"@typescript-eslint/no-unused-vars\": [\n        \"warn\",\n        { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/jest.config.js",
    "content": "/* eslint-env node */\n/* eslint-disable @typescript-eslint/no-var-requires */\nconst UIExtensionsConfig = require(\"@stripe/ui-extension-tools/jest.config.ui-extension\");\n\nmodule.exports = {\n  ...UIExtensionsConfig,\n};\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/package.json",
    "content": "{\n  \"name\": \"com.example.json-render-demo\",\n  \"version\": \"0.0.10\",\n  \"description\": \"Test\",\n  \"private\": true,\n  \"license\": \"~~proprietary~~\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@stripe/ui-extension-sdk\": \"^9.1.0\",\n    \"stripe\": \"^13.11.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"engines\": {\n    \"node\": \">=14\"\n  },\n  \"scripts\": {\n    \"setup\": \"node scripts/setup.mjs\",\n    \"lint\": \"eslint src --max-warnings 0\",\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@stripe/ui-extension-tools\": \"^0.0.1\",\n    \"eslint\": \"^9.39.0\"\n  }\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/scripts/setup.mjs",
    "content": "import { readFileSync, writeFileSync, existsSync } from \"fs\";\nimport { resolve, dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst root = resolve(__dirname, \"..\");\n\nfunction loadEnv() {\n  const envPath = resolve(root, \".env\");\n  if (!existsSync(envPath)) return {};\n  const env = {};\n  for (const line of readFileSync(envPath, \"utf-8\").split(\"\\n\")) {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"#\")) continue;\n    const idx = trimmed.indexOf(\"=\");\n    if (idx === -1) continue;\n    const key = trimmed.slice(0, idx).trim();\n    const val = trimmed.slice(idx + 1).trim();\n    env[key] = val;\n  }\n  return env;\n}\n\nconst env = loadEnv();\nconst templatePath = resolve(root, \"stripe-app.template.json\");\nconst outputPath = resolve(root, \"stripe-app.json\");\n\nconst template = JSON.parse(readFileSync(templatePath, \"utf-8\"));\n\nif (env.STRIPE_APP_ID) {\n  template.id = env.STRIPE_APP_ID;\n  console.log(`Using app ID from .env: ${env.STRIPE_APP_ID}`);\n} else {\n  console.log(`No STRIPE_APP_ID in .env, using template default: ${template.id}`);\n}\n\nif (env.STRIPE_APP_NAME) {\n  template.name = env.STRIPE_APP_NAME;\n}\n\nwriteFileSync(outputPath, JSON.stringify(template, null, 4) + \"\\n\");\nconsole.log(\"Generated stripe-app.json\");\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/config.ts",
    "content": "export const API_GENERATE_URL =\n  \"http://stripe-api-demo.json-render.localhost:1355/api/generate\";\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/render/catalog/actions.ts",
    "content": "import { stripe, formatAmount, formatDate } from \"../../stripe\";\n\n/**\n * Type for setState function\n */\ntype SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n/**\n * Action handlers for Stripe operations\n *\n * These use the real Stripe API via the UI Extension SDK.\n */\nexport const actionHandlers: Record<\n  string,\n  (\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    data: Record<string, unknown>,\n  ) => Promise<void>\n> = {\n  // ===========================================================================\n  // Customer Actions\n  // ===========================================================================\n  fetchCustomers: async (params, setState) => {\n    try {\n      const customers = await stripe.customers.list({\n        limit: (params?.limit as number) ?? 10,\n        email: (params?.email as string) || undefined,\n        starting_after: (params?.startingAfter as string) || undefined,\n      });\n\n      const data = customers.data.map((c) => ({\n        id: c.id,\n        name: c.name ?? c.email ?? \"Unknown\",\n        email: c.email ?? \"\",\n        phone: c.phone ?? \"\",\n        status: c.delinquent ? \"inactive\" : \"active\",\n        created: formatDate(c.created),\n        balance: c.balance,\n        currency: c.currency ?? \"usd\",\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        customers: {\n          data,\n          total: customers.data.length,\n          hasMore: customers.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchCustomers error:\", error);\n    }\n  },\n\n  viewCustomer: async (params, setState) => {\n    if (params?.customerId) {\n      const id = String(params.customerId);\n      setState((prev) => ({\n        ...prev,\n        _actionResult: {\n          action: \"viewCustomer\",\n          message: `Opening customer ${id}`,\n          url: `https://dashboard.stripe.com/customers/${id}`,\n        },\n      }));\n      try {\n        window.open(`https://dashboard.stripe.com/customers/${id}`, \"_blank\");\n      } catch {\n        // blocked in iframe\n      }\n    }\n  },\n\n  createCustomer: async (params, setState) => {\n    try {\n      await stripe.customers.create({\n        email: (params?.email as string) ?? \"\",\n        name: (params?.name as string) ?? undefined,\n        phone: (params?.phone as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"createCustomer error:\", error);\n    }\n  },\n\n  updateCustomer: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.customers.update(params.customerId as string, {\n        email: (params?.email as string) ?? undefined,\n        name: (params?.name as string) ?? undefined,\n        phone: (params?.phone as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"updateCustomer error:\", error);\n    }\n  },\n\n  deleteCustomer: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.customers.del(params.customerId as string);\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"deleteCustomer error:\", error);\n    }\n  },\n\n  searchCustomers: async (params, setState) => {\n    try {\n      const customers = await stripe.customers.search({\n        query: (params?.query as string) ?? \"\",\n        limit: (params?.limit as number) ?? 10,\n      });\n\n      const data = customers.data.map((c) => ({\n        id: c.id,\n        name: c.name ?? c.email ?? \"Unknown\",\n        email: c.email ?? \"\",\n        status: c.delinquent ? \"inactive\" : \"active\",\n        created: formatDate(c.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        searchResults: { customers: data, total: customers.data.length },\n      }));\n    } catch (error) {\n      console.error(\"searchCustomers error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Payment Intent Actions\n  // ===========================================================================\n  fetchPayments: async (params, setState) => {\n    try {\n      const payments = await stripe.paymentIntents.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        starting_after: (params?.startingAfter as string) || undefined,\n      });\n\n      const data = payments.data.map((p) => ({\n        id: p.id,\n        amount: p.amount,\n        currency: p.currency,\n        status: p.status,\n        description: p.description ?? `Payment ${p.id.slice(-8)}`,\n        created: formatDate(p.created),\n        formattedAmount: formatAmount(p.amount, p.currency),\n        customerId: p.customer as string,\n      }));\n\n      const succeeded = payments.data.filter((p) => p.status === \"succeeded\");\n      const totalVolume = succeeded.reduce((sum, p) => sum + p.amount, 0);\n      const successRate =\n        payments.data.length > 0\n          ? ((succeeded.length / payments.data.length) * 100).toFixed(1)\n          : \"0\";\n\n      setState((prev) => ({\n        ...prev,\n        payments: {\n          data,\n          total: payments.data.length,\n          totalVolume: formatAmount(totalVolume, \"usd\"),\n          successRate: `${successRate}%`,\n          hasMore: payments.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchPayments error:\", error);\n    }\n  },\n\n  viewPayment: async (params, setState) => {\n    if (params?.paymentId) {\n      const id = String(params.paymentId);\n      setState((prev) => ({\n        ...prev,\n        _actionResult: {\n          action: \"viewPayment\",\n          message: `Opening payment ${id}`,\n          url: `https://dashboard.stripe.com/payments/${id}`,\n        },\n      }));\n      try {\n        window.open(`https://dashboard.stripe.com/payments/${id}`, \"_blank\");\n      } catch {\n        // blocked in iframe -- state update above still shows feedback\n      }\n    }\n  },\n\n  createPaymentIntent: async (params, setState) => {\n    try {\n      await stripe.paymentIntents.create({\n        amount: (params?.amount as number) ?? 0,\n        currency: (params?.currency as string) ?? \"usd\",\n        customer: (params?.customerId as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"createPaymentIntent error:\", error);\n    }\n  },\n\n  capturePayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.paymentIntents.capture(params.paymentId as string, {\n        amount_to_capture: (params?.amountToCapture as number) ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"capturePayment error:\", error);\n    }\n  },\n\n  cancelPayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.paymentIntents.cancel(params.paymentId as string, {\n        cancellation_reason:\n          (params?.reason as\n            | \"duplicate\"\n            | \"fraudulent\"\n            | \"requested_by_customer\"\n            | \"abandoned\") ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelPayment error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Refund Actions\n  // ===========================================================================\n  fetchRefunds: async (params, setState) => {\n    try {\n      const refunds = await stripe.refunds.list({\n        limit: (params?.limit as number) ?? 10,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n        charge: (params?.chargeId as string) || undefined,\n      });\n\n      const data = refunds.data.map((r) => ({\n        id: r.id,\n        amount: r.amount,\n        currency: r.currency,\n        status: r.status,\n        reason: r.reason,\n        created: formatDate(r.created),\n        formattedAmount: formatAmount(r.amount, r.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        refunds: { data, total: refunds.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchRefunds error:\", error);\n    }\n  },\n\n  refundPayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.refunds.create({\n        payment_intent: params.paymentId as string,\n        amount: (params?.amount as number) ?? undefined,\n        reason:\n          (params?.reason as\n            | \"duplicate\"\n            | \"fraudulent\"\n            | \"requested_by_customer\") ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n      await actionHandlers.fetchRefunds({}, setState, {});\n    } catch (error) {\n      console.error(\"refundPayment error:\", error);\n    }\n  },\n\n  updateRefund: async (params, setState) => {\n    try {\n      if (!params?.refundId) return;\n      await stripe.refunds.update(params.refundId as string, {\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchRefunds({}, setState, {});\n    } catch (error) {\n      console.error(\"updateRefund error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Charge Actions\n  // ===========================================================================\n  fetchCharges: async (params, setState) => {\n    try {\n      const charges = await stripe.charges.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n      });\n\n      const data = charges.data.map((c) => ({\n        id: c.id,\n        amount: c.amount,\n        currency: c.currency,\n        status: c.status,\n        captured: c.captured,\n        description: c.description,\n        created: formatDate(c.created),\n        formattedAmount: formatAmount(c.amount, c.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        charges: { data, total: charges.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCharges error:\", error);\n    }\n  },\n\n  captureCharge: async (params, setState) => {\n    try {\n      if (!params?.chargeId) return;\n      await stripe.charges.capture(params.chargeId as string, {\n        amount: (params?.amount as number) ?? undefined,\n      });\n      await actionHandlers.fetchCharges({}, setState, {});\n    } catch (error) {\n      console.error(\"captureCharge error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Subscription Actions\n  // ===========================================================================\n  fetchSubscriptions: async (params, setState) => {\n    try {\n      const subscriptions = await stripe.subscriptions.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n        customer: (params?.customerId as string) || undefined,\n        price: (params?.priceId as string) || undefined,\n      });\n\n      const data = subscriptions.data.map((s) => ({\n        id: s.id,\n        planName:\n          s.items.data[0]?.price?.nickname ??\n          s.items.data[0]?.price?.id ??\n          \"Plan\",\n        status: s.status,\n        amount: s.items.data[0]?.price?.unit_amount ?? 0,\n        currency: s.items.data[0]?.price?.currency ?? \"usd\",\n        interval: s.items.data[0]?.price?.recurring?.interval ?? \"month\",\n        customerId: s.customer as string,\n        currentPeriodEnd: formatDate(s.current_period_end),\n        cancelAtPeriodEnd: s.cancel_at_period_end,\n      }));\n\n      const active = subscriptions.data.filter(\n        (s) => s.status === \"active\",\n      ).length;\n      const trialing = subscriptions.data.filter(\n        (s) => s.status === \"trialing\",\n      ).length;\n      const pastDue = subscriptions.data.filter(\n        (s) => s.status === \"past_due\",\n      ).length;\n      const canceled = subscriptions.data.filter(\n        (s) => s.status === \"canceled\",\n      ).length;\n\n      setState((prev) => ({\n        ...prev,\n        subscriptions: {\n          data,\n          total: subscriptions.data.length,\n          active,\n          trialing,\n          pastDue,\n          canceled,\n          hasMore: subscriptions.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchSubscriptions error:\", error);\n    }\n  },\n\n  viewSubscription: async (params) => {\n    if (params?.subscriptionId) {\n      window.open(\n        `https://dashboard.stripe.com/subscriptions/${params.subscriptionId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createSubscription: async (params, setState) => {\n    try {\n      if (!params?.customerId || !params?.priceId) return;\n      await stripe.subscriptions.create({\n        customer: params.customerId as string,\n        items: [\n          {\n            price: params.priceId as string,\n            quantity: (params?.quantity as number) ?? 1,\n          },\n        ],\n        trial_period_days: (params?.trialPeriodDays as number) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"createSubscription error:\", error);\n    }\n  },\n\n  updateSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      const updateParams: Record<string, unknown> = {};\n      if (params.priceId) {\n        updateParams.items = [\n          {\n            price: params.priceId as string,\n            quantity: (params?.quantity as number) ?? 1,\n          },\n        ];\n      }\n      if (params.metadata) {\n        updateParams.metadata = params.metadata;\n      }\n      await stripe.subscriptions.update(\n        params.subscriptionId as string,\n        updateParams,\n      );\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"updateSubscription error:\", error);\n    }\n  },\n\n  cancelSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      if (params.immediately) {\n        await stripe.subscriptions.cancel(params.subscriptionId as string);\n      } else {\n        await stripe.subscriptions.update(params.subscriptionId as string, {\n          cancel_at_period_end: true,\n        });\n      }\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelSubscription error:\", error);\n    }\n  },\n\n  pauseSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      await stripe.subscriptions.update(params.subscriptionId as string, {\n        pause_collection: {\n          behavior: \"mark_uncollectible\",\n          resumes_at: (params?.resumeAt as number) ?? undefined,\n        },\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"pauseSubscription error:\", error);\n    }\n  },\n\n  resumeSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      await stripe.subscriptions.update(params.subscriptionId as string, {\n        pause_collection: \"\",\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"resumeSubscription error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Invoice Actions\n  // ===========================================================================\n  fetchInvoices: async (params, setState) => {\n    try {\n      const invoices = await stripe.invoices.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n        customer: (params?.customerId as string) || undefined,\n        subscription: (params?.subscriptionId as string) || undefined,\n      });\n\n      const data = invoices.data.map((inv) => ({\n        id: inv.id,\n        invoiceNumber: inv.number ?? inv.id.slice(-8),\n        amount: inv.amount_due,\n        currency: inv.currency,\n        status: inv.status ?? \"draft\",\n        dueDate: inv.due_date ? formatDate(inv.due_date) : null,\n        customerEmail: inv.customer_email ?? \"\",\n        formattedAmount: formatAmount(inv.amount_due, inv.currency),\n        hostedInvoiceUrl: inv.hosted_invoice_url,\n        pdfUrl: inv.invoice_pdf,\n      }));\n\n      const outstanding = invoices.data\n        .filter((i) => i.status === \"open\")\n        .reduce((sum, i) => sum + i.amount_due, 0);\n      const paid = invoices.data\n        .filter((i) => i.status === \"paid\")\n        .reduce((sum, i) => sum + i.amount_paid, 0);\n      const overdue = invoices.data\n        .filter(\n          (i) =>\n            i.status === \"open\" && i.due_date && i.due_date < Date.now() / 1000,\n        )\n        .reduce((sum, i) => sum + i.amount_due, 0);\n\n      setState((prev) => ({\n        ...prev,\n        invoices: {\n          data,\n          total: invoices.data.length,\n          outstanding: formatAmount(outstanding, \"usd\"),\n          paid: formatAmount(paid, \"usd\"),\n          overdue: formatAmount(overdue, \"usd\"),\n          hasMore: invoices.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchInvoices error:\", error);\n    }\n  },\n\n  viewInvoice: async (params) => {\n    if (params?.invoiceId) {\n      window.open(\n        `https://dashboard.stripe.com/invoices/${params.invoiceId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createInvoice: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.invoices.create({\n        customer: params.customerId as string,\n        description: (params?.description as string) ?? undefined,\n        days_until_due: (params?.daysUntilDue as number) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"createInvoice error:\", error);\n    }\n  },\n\n  addInvoiceItem: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoiceItems.create({\n        invoice: params.invoiceId as string,\n        amount: (params?.amount as number) ?? 0,\n        currency: (params?.currency as string) ?? \"usd\",\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"addInvoiceItem error:\", error);\n    }\n  },\n\n  finalizeInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.finalizeInvoice(params.invoiceId as string, {\n        auto_advance: (params?.autoAdvance as boolean) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"finalizeInvoice error:\", error);\n    }\n  },\n\n  sendInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.sendInvoice(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"sendInvoice error:\", error);\n    }\n  },\n\n  payInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.pay(params.invoiceId as string, {\n        payment_method: (params?.paymentMethodId as string) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"payInvoice error:\", error);\n    }\n  },\n\n  voidInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.voidInvoice(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"voidInvoice error:\", error);\n    }\n  },\n\n  markInvoiceUncollectible: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.markUncollectible(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"markInvoiceUncollectible error:\", error);\n    }\n  },\n\n  downloadInvoicePdf: async (params, _, data) => {\n    try {\n      if (!params?.invoiceId) return;\n      const invoices =\n        (data.invoices as { data: Array<{ id: string; pdfUrl: string }> })\n          ?.data ?? [];\n      const invoice = invoices.find((i) => i.id === params.invoiceId);\n      if (invoice?.pdfUrl) {\n        window.open(invoice.pdfUrl, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"downloadInvoicePdf error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Product & Price Actions\n  // ===========================================================================\n  fetchProducts: async (params, setState) => {\n    try {\n      const products = await stripe.products.list({\n        limit: (params?.limit as number) ?? 10,\n        active: (params?.active as boolean) ?? undefined,\n      });\n\n      const data = products.data.map((p) => ({\n        id: p.id,\n        name: p.name,\n        description: p.description,\n        active: p.active,\n        created: formatDate(p.created),\n        images: p.images,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        products: {\n          data,\n          total: products.data.length,\n          hasMore: products.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchProducts error:\", error);\n    }\n  },\n\n  viewProduct: async (params) => {\n    if (params?.productId) {\n      window.open(\n        `https://dashboard.stripe.com/products/${params.productId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createProduct: async (params, setState) => {\n    try {\n      if (!params?.name) return;\n      await stripe.products.create({\n        name: params.name as string,\n        description: (params?.description as string) ?? undefined,\n        active: (params?.active as boolean) ?? true,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"createProduct error:\", error);\n    }\n  },\n\n  updateProduct: async (params, setState) => {\n    try {\n      if (!params?.productId) return;\n      await stripe.products.update(params.productId as string, {\n        name: (params?.name as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        active: (params?.active as boolean) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"updateProduct error:\", error);\n    }\n  },\n\n  archiveProduct: async (params, setState) => {\n    try {\n      if (!params?.productId) return;\n      await stripe.products.update(params.productId as string, {\n        active: false,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"archiveProduct error:\", error);\n    }\n  },\n\n  fetchPrices: async (params, setState) => {\n    try {\n      const prices = await stripe.prices.list({\n        limit: (params?.limit as number) ?? 10,\n        product: (params?.productId as string) || undefined,\n        active: (params?.active as boolean) ?? undefined,\n        type: (params?.type as \"one_time\" | \"recurring\") || undefined,\n      });\n\n      const data = prices.data.map((p) => ({\n        id: p.id,\n        nickname: p.nickname,\n        unitAmount: p.unit_amount,\n        currency: p.currency,\n        active: p.active,\n        type: p.type,\n        recurring: p.recurring,\n        formattedAmount: formatAmount(p.unit_amount ?? 0, p.currency),\n        productId: typeof p.product === \"string\" ? p.product : p.product?.id,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        prices: { data, total: prices.data.length, hasMore: prices.has_more },\n      }));\n    } catch (error) {\n      console.error(\"fetchPrices error:\", error);\n    }\n  },\n\n  createPrice: async (params, setState) => {\n    try {\n      if (!params?.productId || !params?.unitAmount) return;\n      const recurring = params?.recurring as\n        | { interval: string; intervalCount?: number }\n        | undefined;\n      await stripe.prices.create({\n        product: params.productId as string,\n        unit_amount: params.unitAmount as number,\n        currency: (params?.currency as string) ?? \"usd\",\n        recurring: recurring\n          ? {\n              interval: recurring.interval as \"day\" | \"week\" | \"month\" | \"year\",\n              interval_count: recurring.intervalCount ?? undefined,\n            }\n          : undefined,\n        nickname: (params?.nickname as string) ?? undefined,\n      });\n      await actionHandlers.fetchPrices({}, setState, {});\n    } catch (error) {\n      console.error(\"createPrice error:\", error);\n    }\n  },\n\n  updatePrice: async (params, setState) => {\n    try {\n      if (!params?.priceId) return;\n      await stripe.prices.update(params.priceId as string, {\n        active: (params?.active as boolean) ?? undefined,\n        nickname: (params?.nickname as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchPrices({}, setState, {});\n    } catch (error) {\n      console.error(\"updatePrice error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Balance & Payout Actions\n  // ===========================================================================\n  fetchBalance: async (_, setState) => {\n    try {\n      const balance = await stripe.balance.retrieve();\n\n      const available = balance.available.reduce((sum, b) => sum + b.amount, 0);\n      const pending = balance.pending.reduce((sum, b) => sum + b.amount, 0);\n\n      setState((prev) => ({\n        ...prev,\n        balance: {\n          available,\n          pending,\n          formattedAvailable: formatAmount(available, \"usd\"),\n          formattedPending: formatAmount(pending, \"usd\"),\n          currency: balance.available[0]?.currency ?? \"usd\",\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchBalance error:\", error);\n    }\n  },\n\n  fetchPayouts: async (params, setState) => {\n    try {\n      const payouts = await stripe.payouts.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n      });\n\n      const data = payouts.data.map((p) => ({\n        id: p.id,\n        amount: p.amount,\n        currency: p.currency,\n        status: p.status,\n        arrivalDate: formatDate(p.arrival_date),\n        created: formatDate(p.created),\n        formattedAmount: formatAmount(p.amount, p.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        payouts: {\n          data,\n          total: payouts.data.length,\n          hasMore: payouts.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchPayouts error:\", error);\n    }\n  },\n\n  createPayout: async (params, setState) => {\n    try {\n      if (!params?.amount) return;\n      await stripe.payouts.create({\n        amount: params.amount as number,\n        currency: (params?.currency as string) ?? \"usd\",\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchPayouts({}, setState, {});\n      await actionHandlers.fetchBalance({}, setState, {});\n    } catch (error) {\n      console.error(\"createPayout error:\", error);\n    }\n  },\n\n  cancelPayout: async (params, setState) => {\n    try {\n      if (!params?.payoutId) return;\n      await stripe.payouts.cancel(params.payoutId as string);\n      await actionHandlers.fetchPayouts({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelPayout error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Dispute Actions\n  // ===========================================================================\n  fetchDisputes: async (params, setState) => {\n    try {\n      const disputes = await stripe.disputes.list({\n        limit: (params?.limit as number) ?? 10,\n        charge: (params?.chargeId as string) || undefined,\n      });\n\n      const data = disputes.data.map((d) => ({\n        id: d.id,\n        amount: d.amount,\n        currency: d.currency,\n        status: d.status,\n        reason: d.reason,\n        created: formatDate(d.created),\n        formattedAmount: formatAmount(d.amount, d.currency),\n        evidenceDueBy: d.evidence_details?.due_by\n          ? formatDate(d.evidence_details.due_by)\n          : null,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        disputes: {\n          data,\n          total: disputes.data.length,\n          hasMore: disputes.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchDisputes error:\", error);\n    }\n  },\n\n  viewDispute: async (params) => {\n    if (params?.disputeId) {\n      window.open(\n        `https://dashboard.stripe.com/disputes/${params.disputeId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  updateDispute: async (params, setState) => {\n    try {\n      if (!params?.disputeId) return;\n      const evidence = params?.evidence as Record<string, string> | undefined;\n      await stripe.disputes.update(params.disputeId as string, {\n        evidence: evidence\n          ? {\n              customer_name: evidence.customerName ?? undefined,\n              customer_email_address:\n                evidence.customerEmailAddress ?? undefined,\n              product_description: evidence.productDescription ?? undefined,\n              uncategorized_text: evidence.uncategorizedText ?? undefined,\n            }\n          : undefined,\n        submit: (params?.submit as boolean) ?? undefined,\n      });\n      await actionHandlers.fetchDisputes({}, setState, {});\n    } catch (error) {\n      console.error(\"updateDispute error:\", error);\n    }\n  },\n\n  closeDispute: async (params, setState) => {\n    try {\n      if (!params?.disputeId) return;\n      await stripe.disputes.close(params.disputeId as string);\n      await actionHandlers.fetchDisputes({}, setState, {});\n    } catch (error) {\n      console.error(\"closeDispute error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Payment Method Actions\n  // ===========================================================================\n  fetchPaymentMethods: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      const paymentMethods = await stripe.paymentMethods.list({\n        customer: params.customerId as string,\n        type: (params?.type as \"card\" | \"us_bank_account\") || \"card\",\n      });\n\n      const data = paymentMethods.data.map((pm) => ({\n        id: pm.id,\n        type: pm.type,\n        card: pm.card\n          ? {\n              brand: pm.card.brand,\n              last4: pm.card.last4,\n              expMonth: pm.card.exp_month,\n              expYear: pm.card.exp_year,\n            }\n          : null,\n        created: formatDate(pm.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        paymentMethods: { data, total: paymentMethods.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchPaymentMethods error:\", error);\n    }\n  },\n\n  attachPaymentMethod: async (params, setState) => {\n    try {\n      if (!params?.paymentMethodId || !params?.customerId) return;\n      await stripe.paymentMethods.attach(params.paymentMethodId as string, {\n        customer: params.customerId as string,\n      });\n      await actionHandlers.fetchPaymentMethods(\n        { customerId: params.customerId },\n        setState,\n        {},\n      );\n    } catch (error) {\n      console.error(\"attachPaymentMethod error:\", error);\n    }\n  },\n\n  detachPaymentMethod: async (params, _setState) => {\n    try {\n      if (!params?.paymentMethodId) return;\n      await stripe.paymentMethods.detach(params.paymentMethodId as string);\n    } catch (error) {\n      console.error(\"detachPaymentMethod error:\", error);\n    }\n  },\n\n  setDefaultPaymentMethod: async (params, setState) => {\n    try {\n      if (!params?.customerId || !params?.paymentMethodId) return;\n      await stripe.customers.update(params.customerId as string, {\n        invoice_settings: {\n          default_payment_method: params.paymentMethodId as string,\n        },\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"setDefaultPaymentMethod error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Coupon & Promotion Actions\n  // ===========================================================================\n  fetchCoupons: async (params, setState) => {\n    try {\n      const coupons = await stripe.coupons.list({\n        limit: (params?.limit as number) ?? 10,\n      });\n\n      const data = coupons.data.map((c) => ({\n        id: c.id,\n        name: c.name,\n        percentOff: c.percent_off,\n        amountOff: c.amount_off,\n        currency: c.currency,\n        duration: c.duration,\n        durationInMonths: c.duration_in_months,\n        maxRedemptions: c.max_redemptions,\n        timesRedeemed: c.times_redeemed,\n        valid: c.valid,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        coupons: { data, total: coupons.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCoupons error:\", error);\n    }\n  },\n\n  createCoupon: async (params, setState) => {\n    try {\n      await stripe.coupons.create({\n        percent_off: (params?.percentOff as number) ?? undefined,\n        amount_off: (params?.amountOff as number) ?? undefined,\n        currency: (params?.currency as string) ?? undefined,\n        duration:\n          (params?.duration as \"forever\" | \"once\" | \"repeating\") ?? \"once\",\n        duration_in_months: (params?.durationInMonths as number) ?? undefined,\n        name: (params?.name as string) ?? undefined,\n        max_redemptions: (params?.maxRedemptions as number) ?? undefined,\n      });\n      await actionHandlers.fetchCoupons({}, setState, {});\n    } catch (error) {\n      console.error(\"createCoupon error:\", error);\n    }\n  },\n\n  deleteCoupon: async (params, setState) => {\n    try {\n      if (!params?.couponId) return;\n      await stripe.coupons.del(params.couponId as string);\n      await actionHandlers.fetchCoupons({}, setState, {});\n    } catch (error) {\n      console.error(\"deleteCoupon error:\", error);\n    }\n  },\n\n  fetchPromotionCodes: async (params, setState) => {\n    try {\n      const promoCodes = await stripe.promotionCodes.list({\n        limit: (params?.limit as number) ?? 10,\n        coupon: (params?.couponId as string) || undefined,\n        active: (params?.active as boolean) ?? undefined,\n      });\n\n      const data = promoCodes.data.map((p) => ({\n        id: p.id,\n        code: p.code,\n        couponId: typeof p.coupon === \"string\" ? p.coupon : p.coupon?.id,\n        active: p.active,\n        maxRedemptions: p.max_redemptions,\n        timesRedeemed: p.times_redeemed,\n        expiresAt: p.expires_at ? formatDate(p.expires_at) : null,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        promotionCodes: { data, total: promoCodes.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchPromotionCodes error:\", error);\n    }\n  },\n\n  createPromotionCode: async (params, setState) => {\n    try {\n      if (!params?.couponId) return;\n      await stripe.promotionCodes.create({\n        coupon: params.couponId as string,\n        code: (params?.code as string) ?? undefined,\n        max_redemptions: (params?.maxRedemptions as number) ?? undefined,\n        expires_at: (params?.expiresAt as number) ?? undefined,\n      });\n      await actionHandlers.fetchPromotionCodes({}, setState, {});\n    } catch (error) {\n      console.error(\"createPromotionCode error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Checkout Session Actions\n  // ===========================================================================\n  createCheckoutSession: async (params, setState) => {\n    try {\n      const lineItems =\n        (params?.lineItems as Array<{ priceId: string; quantity: number }>) ??\n        [];\n      const session = await stripe.checkout.sessions.create({\n        mode:\n          (params?.mode as \"payment\" | \"subscription\" | \"setup\") ?? \"payment\",\n        line_items: lineItems.map((li) => ({\n          price: li.priceId,\n          quantity: li.quantity,\n        })),\n        success_url: (params?.successUrl as string) ?? \"\",\n        cancel_url: (params?.cancelUrl as string) ?? \"\",\n        customer: (params?.customerId as string) ?? undefined,\n      });\n\n      setState((prev) => ({\n        ...prev,\n        checkoutSession: { id: session.id, url: session.url },\n      }));\n\n      if (session.url) {\n        window.open(session.url, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"createCheckoutSession error:\", error);\n    }\n  },\n\n  fetchCheckoutSessions: async (params, setState) => {\n    try {\n      const sessions = await stripe.checkout.sessions.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n      });\n\n      const data = sessions.data.map((s) => ({\n        id: s.id,\n        mode: s.mode,\n        status: s.status,\n        amountTotal: s.amount_total,\n        currency: s.currency,\n        customerEmail: s.customer_email,\n        url: s.url,\n        created: formatDate(s.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        checkoutSessions: { data, total: sessions.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCheckoutSessions error:\", error);\n    }\n  },\n\n  expireCheckoutSession: async (params, setState) => {\n    try {\n      if (!params?.sessionId) return;\n      await stripe.checkout.sessions.expire(params.sessionId as string);\n      await actionHandlers.fetchCheckoutSessions({}, setState, {});\n    } catch (error) {\n      console.error(\"expireCheckoutSession error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Billing Portal Actions\n  // ===========================================================================\n  createBillingPortalSession: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      const session = await stripe.billingPortal.sessions.create({\n        customer: params.customerId as string,\n        return_url: (params?.returnUrl as string) ?? window.location.href,\n      });\n\n      setState((prev) => ({\n        ...prev,\n        billingPortalSession: { url: session.url },\n      }));\n\n      if (session.url) {\n        window.open(session.url, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"createBillingPortalSession error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Event Actions\n  // ===========================================================================\n  fetchEvents: async (params, setState) => {\n    try {\n      const events = await stripe.events.list({\n        limit: (params?.limit as number) ?? 10,\n        type: (params?.type as string) || undefined,\n        created: {\n          gte: (params?.createdGte as number) || undefined,\n          lte: (params?.createdLte as number) || undefined,\n        },\n      });\n\n      const data = events.data.map((e) => ({\n        id: e.id,\n        type: e.type,\n        created: formatDate(e.created),\n        livemode: e.livemode,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        events: { data, total: events.data.length, hasMore: events.has_more },\n      }));\n    } catch (error) {\n      console.error(\"fetchEvents error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Setup Intent Actions\n  // ===========================================================================\n  createSetupIntent: async (params, setState) => {\n    try {\n      const setupIntent = await stripe.setupIntents.create({\n        customer: (params?.customerId as string) ?? undefined,\n        payment_method_types: (params?.paymentMethodTypes as string[]) ?? [\n          \"card\",\n        ],\n        usage: (params?.usage as \"on_session\" | \"off_session\") ?? \"off_session\",\n      });\n\n      setState((prev) => ({\n        ...prev,\n        setupIntent: {\n          id: setupIntent.id,\n          clientSecret: setupIntent.client_secret,\n        },\n      }));\n    } catch (error) {\n      console.error(\"createSetupIntent error:\", error);\n    }\n  },\n\n  fetchSetupIntents: async (params, setState) => {\n    try {\n      const setupIntents = await stripe.setupIntents.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n      });\n\n      const data = setupIntents.data.map((si) => ({\n        id: si.id,\n        status: si.status,\n        usage: si.usage,\n        created: formatDate(si.created),\n        paymentMethodTypes: si.payment_method_types,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        setupIntents: { data, total: setupIntents.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchSetupIntents error:\", error);\n    }\n  },\n\n  cancelSetupIntent: async (params, setState) => {\n    try {\n      if (!params?.setupIntentId) return;\n      await stripe.setupIntents.cancel(params.setupIntentId as string);\n      await actionHandlers.fetchSetupIntents({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelSetupIntent error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Tax Rate Actions\n  // ===========================================================================\n  fetchTaxRates: async (params, setState) => {\n    try {\n      const taxRates = await stripe.taxRates.list({\n        limit: (params?.limit as number) ?? 10,\n        active: (params?.active as boolean) ?? undefined,\n        inclusive: (params?.inclusive as boolean) ?? undefined,\n      });\n\n      const data = taxRates.data.map((tr) => ({\n        id: tr.id,\n        displayName: tr.display_name,\n        percentage: tr.percentage,\n        inclusive: tr.inclusive,\n        jurisdiction: tr.jurisdiction,\n        description: tr.description,\n        active: tr.active,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        taxRates: { data, total: taxRates.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchTaxRates error:\", error);\n    }\n  },\n\n  createTaxRate: async (params, setState) => {\n    try {\n      if (!params?.displayName || params?.percentage === undefined) return;\n      await stripe.taxRates.create({\n        display_name: params.displayName as string,\n        percentage: params.percentage as number,\n        inclusive: (params?.inclusive as boolean) ?? false,\n        jurisdiction: (params?.jurisdiction as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchTaxRates({}, setState, {});\n    } catch (error) {\n      console.error(\"createTaxRate error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Data & Refresh Actions\n  // ===========================================================================\n  refreshData: async (_, setState) => {\n    await Promise.all([\n      actionHandlers.fetchCustomers({}, setState, {}),\n      actionHandlers.fetchPayments({}, setState, {}),\n      actionHandlers.fetchSubscriptions({}, setState, {}),\n      actionHandlers.fetchInvoices({}, setState, {}),\n    ]);\n  },\n\n  refreshCustomers: async (_, setState) => {\n    await actionHandlers.fetchCustomers({}, setState, {});\n  },\n\n  refreshPayments: async (_, setState) => {\n    await actionHandlers.fetchPayments({}, setState, {});\n  },\n\n  refreshSubscriptions: async (_, setState) => {\n    await actionHandlers.fetchSubscriptions({}, setState, {});\n  },\n\n  refreshInvoices: async (_, setState) => {\n    await actionHandlers.fetchInvoices({}, setState, {});\n  },\n\n  exportData: async (params, _, data) => {\n    try {\n      const format = (params?.format as string) ?? \"json\";\n      const dataType = (params?.dataType as string) ?? \"customers\";\n      const exportData = (data[dataType] as { data: unknown[] })?.data ?? [];\n\n      if (format === \"json\") {\n        const blob = new Blob([JSON.stringify(exportData, null, 2)], {\n          type: \"application/json\",\n        });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = `${dataType}.json`;\n        a.click();\n        URL.revokeObjectURL(url);\n      } else if (format === \"csv\") {\n        if (exportData.length === 0) return;\n        const headers = Object.keys(exportData[0] as object);\n        const csv = [\n          headers.join(\",\"),\n          ...exportData.map((row) =>\n            headers\n              .map((h) =>\n                JSON.stringify((row as Record<string, unknown>)[h] ?? \"\"),\n              )\n              .join(\",\"),\n          ),\n        ].join(\"\\n\");\n        const blob = new Blob([csv], { type: \"text/csv\" });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = `${dataType}.csv`;\n        a.click();\n        URL.revokeObjectURL(url);\n      }\n    } catch (error) {\n      console.error(\"exportData error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Navigation Actions\n  // ===========================================================================\n  navigate: async (_params) => {\n    // In a real app, this would use the router\n  },\n\n  openDashboard: async (params) => {\n    const page = (params?.page as string) ?? \"home\";\n    const paths: Record<string, string> = {\n      home: \"\",\n      payments: \"payments\",\n      customers: \"customers\",\n      products: \"products\",\n      subscriptions: \"subscriptions\",\n      invoices: \"invoices\",\n      connect: \"connect/accounts\",\n      reports: \"reports\",\n      developers: \"developers\",\n    };\n    window.open(`https://dashboard.stripe.com/${paths[page] ?? \"\"}`, \"_blank\");\n  },\n\n  openExternalLink: async (params) => {\n    if (params?.url) {\n      window.open(params.url as string, \"_blank\");\n    }\n  },\n\n  // ===========================================================================\n  // Form Actions\n  // ===========================================================================\n  submitForm: async (_params, _, _data) => {\n    // Implementation depends on form handling logic\n  },\n\n  resetForm: async (params, setState) => {\n    const formId = params?.formId as string;\n    if (formId) {\n      setState((prev) => ({ ...prev, [formId]: {} }));\n    }\n  },\n\n  validateForm: async (_params, _, _data) => {\n    // Implementation depends on form validation logic\n  },\n\n  setFormValue: async (params, setState) => {\n    if (params?.statePath) {\n      setState((prev) => ({\n        ...prev,\n        [params.statePath as string]: params?.value,\n      }));\n    }\n  },\n\n  // ===========================================================================\n  // UI Actions\n  // ===========================================================================\n  showToast: async (_params) => {\n    // In a real app, use showToast from @stripe/ui-extension-sdk/utils\n  },\n\n  copyToClipboard: async (params) => {\n    if (params?.text) {\n      await navigator.clipboard.writeText(params.text as string);\n    }\n  },\n\n  setLoading: async (params, setState) => {\n    setState((prev) => ({\n      ...prev,\n      loading: params?.loading,\n      loadingMessage: params?.message,\n    }));\n  },\n\n  // ===========================================================================\n  // Filter & Sort Actions\n  // ===========================================================================\n  setFilter: async (params, setState) => {\n    if (params?.key) {\n      setState((prev) => ({\n        ...prev,\n        filters: {\n          ...(prev.filters as Record<string, unknown>),\n          [params.key as string]: params?.value,\n        },\n      }));\n    }\n  },\n\n  clearFilters: async (_, setState) => {\n    setState((prev) => ({ ...prev, filters: {} }));\n  },\n\n  setSort: async (params, setState) => {\n    setState((prev) => ({\n      ...prev,\n      sort: { field: params?.field, direction: params?.direction },\n    }));\n  },\n\n  setPageSize: async (params, setState) => {\n    setState((prev) => ({ ...prev, pageSize: params?.size }));\n  },\n\n  goToPage: async (params, setState) => {\n    setState((prev) => ({ ...prev, currentPage: params?.page }));\n  },\n};\n\n// =============================================================================\n// Execute Action\n// =============================================================================\n\ntype SetStateFn = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n/**\n * Execute an action by name with the given parameters.\n */\nexport async function executeAction(\n  actionName: string,\n  params: Record<string, unknown> | undefined,\n  setState: SetStateFn,\n  data: Record<string, unknown> = {},\n): Promise<void> {\n  const handler = actionHandlers[actionName];\n  if (handler) {\n    await handler(params, setState, data);\n  } else {\n    console.warn(\"Unknown action:\", actionName);\n  }\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/render/catalog/components.tsx",
    "content": "/**\n * Stripe UIXT Component Implementations\n *\n * Maps json-render catalog components to Stripe UI Extension SDK components.\n */\nimport type { FunctionComponent } from \"react\";\nimport type { ComponentRenderProps } from \"@json-render/react\";\nimport {\n  Box,\n  Badge as UIBadge,\n  Banner as UIBanner,\n  Button as UIButton,\n  Checkbox as UICheckbox,\n  DateField as UIDateField,\n  Divider as UIDivider,\n  Icon as UIIcon,\n  Img as UIImg,\n  Inline as UIInline,\n  Link as UILink,\n  List as UIList,\n  ListItem as UIListItem,\n  Menu as UIMenu,\n  MenuGroup as UIMenuGroup,\n  MenuItem as UIMenuItem,\n  PropertyList as UIPropertyList,\n  PropertyListItem as UIPropertyListItem,\n  Radio as UIRadio,\n  Select as UISelect,\n  Spinner as UISpinner,\n  Switch as UISwitch,\n  TaskList as UITaskList,\n  TaskListItem as UITaskListItem,\n  TextArea as UITextArea,\n  TextField as UITextField,\n  Tooltip as UITooltip,\n  Accordion as UIAccordion,\n  AccordionItem as UIAccordionItem,\n  BarChart as UIBarChart,\n  LineChart as UILineChart,\n  Sparkline as UISparkline,\n  ButtonGroup as UIButtonGroup,\n  Chip as UIChip,\n  ChipList as UIChipList,\n  Table,\n  TableHead,\n  TableBody,\n  TableRow,\n  TableHeaderCell,\n  TableCell,\n} from \"@stripe/ui-extension-sdk/ui\";\n\n// Helper to format currency\nconst formatCurrency = (amount: number, currency = \"usd\"): string => {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: currency.toUpperCase(),\n  }).format(amount / 100);\n};\n\n// Extended props type for components with data access\ntype ExtendedRenderProps<P = Record<string, unknown>> =\n  ComponentRenderProps<P> & {\n    state?: Record<string, unknown>;\n    getValue?: (path: string) => unknown;\n    onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  };\n\n// NOTE: All interactive components use `emit` to fire named events.\n// The renderer resolves events to action bindings from the element's `on` field.\n\n// =========================================================================\n// Layout Components\n// =========================================================================\nexport const Stack: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const {\n    direction = \"vertical\",\n    gap = \"medium\",\n    distribute,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: direction === \"horizontal\" ? \"x\" : \"y\",\n        gap: gap as \"xsmall\" | \"small\" | \"medium\" | \"large\" | \"xlarge\",\n        ...(distribute === \"space-between\" && {\n          distribute: \"space-between\" as const,\n        }),\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nexport const Inline: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIInline>{children}</UIInline>;\n};\n\nexport const Divider: FunctionComponent<ExtendedRenderProps> = () => {\n  return <UIDivider />;\n};\n\nexport const Accordion: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIAccordion>{children}</UIAccordion>;\n};\n\nexport const AccordionItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { title, subtitle, defaultOpen } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIAccordionItem\n      title={String(title || \"\")}\n      subtitle={subtitle ? String(subtitle) : undefined}\n      defaultOpen={Boolean(defaultOpen) || undefined}\n    >\n      {children}\n    </UIAccordionItem>\n  );\n};\n\n// =========================================================================\n// Typography Components\n// =========================================================================\nexport const Heading: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { text, size = \"large\" } = element.props as Record<string, unknown>;\n\n  const fontMap: Record<\n    string,\n    \"heading\" | \"title\" | \"subtitle\" | \"subheading\" | \"body\"\n  > = {\n    xsmall: \"body\",\n    small: \"subheading\",\n    medium: \"subtitle\",\n    large: \"title\",\n    xlarge: \"heading\",\n  };\n\n  return (\n    <Box css={{ font: fontMap[size as string] || \"title\", fontWeight: \"bold\" }}>\n      {String(text || \"\")}\n    </Box>\n  );\n};\n\nexport const Text: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    content,\n    color = \"primary\",\n    size = \"medium\",\n    weight = \"regular\",\n  } = element.props as Record<string, unknown>;\n\n  const colorMap: Record<\n    string,\n    | \"primary\"\n    | \"secondary\"\n    | \"disabled\"\n    | \"critical\"\n    | \"success\"\n    | \"attention\"\n    | \"info\"\n  > = {\n    primary: \"primary\",\n    secondary: \"secondary\",\n    disabled: \"disabled\",\n    critical: \"critical\",\n    success: \"success\",\n    warning: \"attention\",\n    info: \"info\",\n  };\n\n  const fontMap: Record<string, \"caption\" | \"body\" | \"subtitle\" | \"title\"> = {\n    xsmall: \"caption\",\n    small: \"caption\",\n    medium: \"body\",\n    large: \"subtitle\",\n  };\n\n  const weightMap: Record<string, \"regular\" | \"semibold\" | \"bold\"> = {\n    regular: \"regular\",\n    medium: \"regular\",\n    semibold: \"semibold\",\n    bold: \"bold\",\n  };\n\n  return (\n    <Box\n      css={{\n        font: fontMap[size as string] || \"body\",\n        color: colorMap[color as string] || \"primary\",\n        fontWeight: weightMap[weight as string] || \"regular\",\n      }}\n    >\n      {String(content || \"\")}\n    </Box>\n  );\n};\n\n// =========================================================================\n// Data Display Components\n// =========================================================================\nexport const Metric: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    value,\n    change,\n    changeType = \"neutral\",\n  } = element.props as Record<string, unknown>;\n\n  const changeColors: Record<string, \"success\" | \"critical\" | \"secondary\"> = {\n    positive: \"success\",\n    negative: \"critical\",\n    neutral: \"secondary\",\n  };\n\n  return (\n    <Box css={{ stack: \"y\", gap: \"xsmall\" }}>\n      <Box css={{ font: \"caption\", color: \"secondary\" }}>\n        {String(label || \"\")}\n      </Box>\n      <Box css={{ font: \"heading\", fontWeight: \"bold\" }}>\n        {String(value || \"\")}\n      </Box>\n      {change && (\n        <Box\n          css={{\n            font: \"caption\",\n            color: changeColors[changeType as string] || \"secondary\",\n          }}\n        >\n          {changeType === \"positive\"\n            ? \"↑\"\n            : changeType === \"negative\"\n              ? \"↓\"\n              : \"\"}{\" \"}\n          {String(change)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nconst BADGE_TYPES = new Set([\n  \"neutral\",\n  \"urgent\",\n  \"warning\",\n  \"negative\",\n  \"positive\",\n  \"info\",\n]);\nconst BADGE_ALIAS: Record<string, string> = {\n  success: \"positive\",\n  error: \"negative\",\n  danger: \"negative\",\n  critical: \"urgent\",\n  default: \"neutral\",\n  primary: \"info\",\n};\n\nfunction coerceBadgeType(\n  raw: unknown,\n): \"neutral\" | \"urgent\" | \"warning\" | \"negative\" | \"positive\" | \"info\" {\n  const s = String(raw ?? \"neutral\");\n  if (BADGE_TYPES.has(s))\n    return s as\n      | \"neutral\"\n      | \"urgent\"\n      | \"warning\"\n      | \"negative\"\n      | \"positive\"\n      | \"info\";\n  return (BADGE_ALIAS[s] ?? \"neutral\") as\n    | \"neutral\"\n    | \"urgent\"\n    | \"warning\"\n    | \"negative\"\n    | \"positive\"\n    | \"info\";\n}\n\nexport const Badge: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { label, type = \"neutral\" } = element.props as Record<string, unknown>;\n  return <UIBadge type={coerceBadgeType(type)}>{String(label || \"\")}</UIBadge>;\n};\n\nexport const Icon: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { name, size = \"medium\" } = element.props as Record<string, unknown>;\n  const sizeMap: Record<string, \"xsmall\" | \"small\" | \"medium\" | \"large\"> = {\n    xsmall: \"xsmall\",\n    small: \"small\",\n    medium: \"medium\",\n    large: \"large\",\n  };\n  return (\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe UIXT Icon name type is complex\n    <UIIcon name={name as any} size={sizeMap[size as string] || \"medium\"} />\n  );\n};\n\nexport const Img: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { src, alt, width, height } = element.props as Record<string, unknown>;\n  return (\n    <UIImg\n      src={String(src || \"\")}\n      alt={String(alt || \"\")}\n      width={width as number | undefined}\n      height={height as number | undefined}\n    />\n  );\n};\n\nexport const Spinner: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { size = \"medium\" } = element.props as Record<string, unknown>;\n  return <UISpinner size={size as \"small\" | \"medium\" | \"large\"} />;\n};\n\n// =========================================================================\n// Feedback Components\n// =========================================================================\nconst BANNER_TYPES = new Set([\"default\", \"caution\", \"critical\"]);\nconst BANNER_ALIAS: Record<string, string> = {\n  info: \"default\",\n  warning: \"caution\",\n  error: \"critical\",\n  danger: \"critical\",\n  success: \"default\",\n};\n\nfunction coerceBannerType(raw: unknown): \"default\" | \"caution\" | \"critical\" {\n  const s = String(raw ?? \"default\");\n  if (BANNER_TYPES.has(s)) return s as \"default\" | \"caution\" | \"critical\";\n  return (BANNER_ALIAS[s] ?? \"default\") as \"default\" | \"caution\" | \"critical\";\n}\n\nexport const Banner: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    title,\n    description,\n    type = \"default\",\n  } = element.props as Record<string, unknown>;\n  return (\n    <UIBanner\n      title={title ? String(title) : undefined}\n      description={description ? String(description) : undefined}\n      type={coerceBannerType(type)}\n    />\n  );\n};\n\n// =========================================================================\n// List Components\n// =========================================================================\nexport const List: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n  emit,\n}) => {\n  return <UIList onAction={(_id) => emit(\"select\")}>{children}</UIList>;\n};\n\nexport const ListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    title,\n    secondaryTitle,\n    value,\n    id,\n    size = \"default\",\n  } = element.props as Record<string, unknown>;\n  return (\n    <UIListItem\n      title={<Box>{String(title || \"\")}</Box>}\n      secondaryTitle={\n        secondaryTitle ? <Box>{String(secondaryTitle)}</Box> : undefined\n      }\n      value={value ? <Box>{String(value)}</Box> : undefined}\n      id={id ? String(id) : undefined}\n      size={size as \"default\" | \"large\"}\n    />\n  );\n};\n\nexport const PropertyList: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { orientation = \"vertical\" } = element.props as Record<string, unknown>;\n  return (\n    <UIPropertyList orientation={orientation as \"vertical\" | \"horizontal\"}>\n      {children}\n    </UIPropertyList>\n  );\n};\n\nexport const PropertyListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { label, value } = element.props as Record<string, unknown>;\n  return (\n    <UIPropertyListItem\n      label={String(label || \"\")}\n      value={String(value || \"\")}\n    />\n  );\n};\n\nexport const TaskList: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UITaskList>{children}</UITaskList>;\n};\n\nexport const TaskListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    title,\n    status = \"not-started\",\n    action,\n  } = element.props as Record<string, unknown>;\n  return (\n    <UITaskListItem\n      title={String(title || \"\")}\n      status={status as \"not-started\" | \"in-progress\" | \"blocked\" | \"complete\"}\n      onPress={action ? () => emit(\"press\") : undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Menu Components\n// =========================================================================\nexport const Menu: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n  emit,\n}) => {\n  const { triggerLabel } = element.props as Record<string, unknown>;\n  return (\n    <UIMenu\n      trigger={\n        <UIButton type=\"secondary\">{String(triggerLabel || \"Menu\")}</UIButton>\n      }\n      onAction={(_id) => emit(\"select\")}\n    >\n      {children}\n    </UIMenu>\n  );\n};\n\nexport const MenuItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const { label, id, disabled, action } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIMenuItem\n      id={String(id || \"\")}\n      disabled={Boolean(disabled) || undefined}\n      onAction={action ? () => emit(\"select\") : undefined}\n    >\n      {String(label || \"\")}\n    </UIMenuItem>\n  );\n};\n\nexport const MenuGroup: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIMenuGroup>{children}</UIMenuGroup>;\n};\n\n// =========================================================================\n// Form Components\n// =========================================================================\nexport const TextField: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    placeholder,\n    description,\n    error,\n    value = \"\",\n    size = \"medium\",\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UITextField\n      label={String(label || \"\")}\n      placeholder={placeholder ? String(placeholder) : undefined}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const TextArea: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    placeholder,\n    description,\n    error,\n    value = \"\",\n    rows = 3,\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UITextArea\n      label={String(label || \"\")}\n      placeholder={placeholder ? String(placeholder) : undefined}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      rows={Number(rows)}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Select: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    description,\n    error,\n    value = \"\",\n    options = [],\n    size = \"medium\",\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n  const opts = options as Array<{ value: string; label: string }>;\n\n  return (\n    <UISelect\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    >\n      {opts.map((opt: { value: string; label: string }) => (\n        <option key={opt.value} value={opt.value}>\n          {opt.label}\n        </option>\n      ))}\n    </UISelect>\n  );\n};\n\nexport const Checkbox: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { label, description, error, checked, defaultChecked, disabled } =\n    element.props as Record<string, unknown>;\n  const isChecked = checked ?? defaultChecked;\n\n  return (\n    <UICheckbox\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      checked={Boolean(isChecked) || undefined}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Radio: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    description,\n    value: selectedValue,\n    optionValue,\n    name,\n    disabled,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UIRadio\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      value={String(optionValue ?? \"\")}\n      name={String(name || \"\")}\n      checked={String(selectedValue ?? \"\") === String(optionValue ?? \"\")}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Switch: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { label, description, checked, defaultChecked, disabled } =\n    element.props as Record<string, unknown>;\n  const isChecked = checked ?? defaultChecked;\n\n  return (\n    <UISwitch\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      checked={Boolean(isChecked) || undefined}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const DateField: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    description,\n    error,\n    value = \"\",\n    size = \"medium\",\n    disabled,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UIDateField\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Button Components\n// =========================================================================\nexport const Button: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n  onAction,\n}) => {\n  const {\n    label,\n    action,\n    actionParams,\n    type = \"primary\",\n    size = \"medium\",\n    disabled,\n    pending,\n    href,\n  } = element.props as Record<string, unknown>;\n\n  const handlePress = action\n    ? () => {\n        emit(\"press\");\n        if (onAction) {\n          onAction(\n            String(action),\n            actionParams as Record<string, unknown> | undefined,\n          );\n        }\n      }\n    : undefined;\n\n  return (\n    <UIButton\n      type={type as \"primary\" | \"secondary\" | \"destructive\"}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      pending={Boolean(pending) || undefined}\n      href={href ? String(href) : undefined}\n      onPress={handlePress}\n    >\n      {String(label || \"\")}\n    </UIButton>\n  );\n};\n\nexport const ButtonGroup: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIButtonGroup>{children}</UIButtonGroup>;\n};\n\nexport const Link: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    href,\n    type = \"primary\",\n    external,\n  } = element.props as Record<string, unknown>;\n  return (\n    <UILink\n      href={String(href || \"\")}\n      type={type as \"primary\" | \"secondary\"}\n      external={Boolean(external) || undefined}\n    >\n      {String(label || \"\")}\n    </UILink>\n  );\n};\n\n// =========================================================================\n// Chart Components\n// =========================================================================\nexport const BarChart: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    colorKey,\n    showAxis = \"both\",\n    showGrid = \"none\",\n    showLegend,\n    showTooltip = true,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return (\n      <Box css={{ color: \"secondary\", font: \"caption\" }}>No data available</Box>\n    );\n  }\n\n  return (\n    <UIBarChart\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      color={colorKey ? String(colorKey) : undefined}\n      axis={coerceAxisGrid(showAxis, \"both\")}\n      grid={coerceAxisGrid(showGrid, \"none\")}\n      legend={Boolean(showLegend) || undefined}\n      tooltip={Boolean(showTooltip)}\n    />\n  );\n};\n\nfunction coerceAxisGrid(\n  val: unknown,\n  fallback: string,\n): \"x\" | \"y\" | \"both\" | \"none\" {\n  if (val === true) return \"both\";\n  if (val === false) return \"none\";\n  const s = String(val ?? fallback);\n  if ([\"x\", \"y\", \"both\", \"none\"].includes(s))\n    return s as \"x\" | \"y\" | \"both\" | \"none\";\n  return fallback as \"x\" | \"y\" | \"both\" | \"none\";\n}\n\nexport const LineChart: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    colorKey,\n    showAxis = \"both\",\n    showGrid = \"none\",\n    showLegend,\n    showTooltip = true,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return (\n      <Box css={{ color: \"secondary\", font: \"caption\" }}>No data available</Box>\n    );\n  }\n\n  return (\n    <UILineChart\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      color={colorKey ? String(colorKey) : undefined}\n      axis={coerceAxisGrid(showAxis, \"both\")}\n      grid={coerceAxisGrid(showGrid, \"none\")}\n      legend={Boolean(showLegend) || undefined}\n      tooltip={Boolean(showTooltip)}\n    />\n  );\n};\n\nexport const Sparkline: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    showTooltip,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return <Box css={{ color: \"secondary\", font: \"caption\" }}>—</Box>;\n  }\n\n  return (\n    <UISparkline\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      tooltip={Boolean(showTooltip) || undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Table Component\n// =========================================================================\nexport const DataTable: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    title,\n    data = [],\n    columns = [],\n    emptyMessage,\n  } = element.props as Record<string, unknown>;\n\n  const tableData = Array.isArray(data) ? data : [];\n  const cols = columns as Array<{ key: string; label: string }>;\n\n  if (!tableData || tableData.length === 0) {\n    return (\n      <Box css={{ stack: \"y\", gap: \"small\" }}>\n        {title && (\n          <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n            {String(title)}\n          </Box>\n        )}\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          {String(emptyMessage || \"No data\")}\n        </Box>\n      </Box>\n    );\n  }\n\n  return (\n    <Box css={{ stack: \"y\", gap: \"small\" }}>\n      {title && (\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(title)}\n        </Box>\n      )}\n      <Table>\n        <TableHead>\n          <TableRow>\n            {cols.map((col: { key: string; label: string }) => (\n              <TableHeaderCell key={col.key}>{col.label}</TableHeaderCell>\n            ))}\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          {tableData.map((row, idx) => (\n            <TableRow key={idx}>\n              {cols.map((col: { key: string; label: string }) => (\n                <TableCell key={col.key}>\n                  {String(row[col.key] ?? \"\")}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n        </TableBody>\n      </Table>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Stripe-Specific Card Components\n// =========================================================================\nexport const CustomerCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    name,\n    email,\n    status = \"active\",\n    customerId,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(name || \"\")}\n        </Box>\n        <UIBadge type={status === \"active\" ? \"positive\" : \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ color: \"secondary\" }}>{String(email || \"\")}</Box>\n      {customerId && (\n        <UIButton type=\"secondary\" size=\"small\" onPress={() => emit(\"press\")}>\n          View Details\n        </UIButton>\n      )}\n    </Box>\n  );\n};\n\nexport const PaymentCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"succeeded\",\n    description,\n    paymentId,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    succeeded: \"positive\",\n    pending: \"warning\",\n    failed: \"negative\",\n    canceled: \"neutral\",\n    requires_action: \"info\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      {description && (\n        <Box css={{ color: \"secondary\" }}>{String(description)}</Box>\n      )}\n      {paymentId && (\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <UIButton type=\"secondary\" size=\"small\" onPress={() => emit(\"press\")}>\n            View\n          </UIButton>\n          <UIButton\n            type=\"destructive\"\n            size=\"small\"\n            onPress={() => emit(\"press\")}\n          >\n            Refund\n          </UIButton>\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const SubscriptionCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    planName,\n    status = \"active\",\n    amount,\n    currency = \"usd\",\n    interval = \"month\",\n    currentPeriodEnd,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    active: \"positive\",\n    trialing: \"info\",\n    past_due: \"warning\",\n    canceled: \"neutral\",\n    unpaid: \"negative\",\n    incomplete: \"warning\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(planName || \"\")}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n        {formatCurrency(Number(amount) || 0, String(currency))}/\n        {String(interval)}\n      </Box>\n      {currentPeriodEnd && (\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          Renews: {String(currentPeriodEnd)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const InvoiceCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    invoiceNumber,\n    amount,\n    currency = \"usd\",\n    status = \"open\",\n    dueDate,\n    customerEmail,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    draft: \"neutral\",\n    open: \"info\",\n    paid: \"positive\",\n    void: \"neutral\",\n    uncollectible: \"negative\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(invoiceNumber || \"\")}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n        {formatCurrency(Number(amount) || 0, String(currency))}\n      </Box>\n      {customerEmail && (\n        <Box css={{ color: \"secondary\" }}>{String(customerEmail)}</Box>\n      )}\n      {dueDate && (\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          Due: {String(dueDate)}\n        </Box>\n      )}\n      {status === \"open\" && (\n        <UIButton type=\"primary\" size=\"small\" onPress={() => emit(\"press\")}>\n          Send Invoice\n        </UIButton>\n      )}\n    </Box>\n  );\n};\n\nexport const RefundCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"succeeded\",\n    reason,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\"\n  > = {\n    pending: \"warning\",\n    succeeded: \"positive\",\n    failed: \"negative\",\n    canceled: \"neutral\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      {reason && <Box css={{ color: \"secondary\" }}>{String(reason)}</Box>}\n    </Box>\n  );\n};\n\nexport const DisputeCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"needs_response\",\n    reason,\n    dueDate,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"urgent\" | \"info\"\n  > = {\n    warning_needs_response: \"urgent\",\n    warning_under_review: \"warning\",\n    warning_closed: \"neutral\",\n    needs_response: \"urgent\",\n    under_review: \"warning\",\n    won: \"positive\",\n    lost: \"negative\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"critical\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"warning\"}>\n          {String(status).replace(/_/g, \" \")}\n        </UIBadge>\n      </Box>\n      {reason && <Box css={{ color: \"secondary\" }}>{String(reason)}</Box>}\n      {dueDate && (\n        <Box css={{ color: \"critical\", font: \"caption\" }}>\n          Response due: {String(dueDate)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const BalanceCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    available,\n    pending,\n    currency = \"usd\",\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>Balance</Box>\n      <Box css={{ stack: \"x\", distribute: \"space-between\" }}>\n        <Box>\n          <Box css={{ font: \"caption\", color: \"secondary\" }}>Available</Box>\n          <Box css={{ font: \"title\", fontWeight: \"bold\", color: \"success\" }}>\n            {formatCurrency(Number(available) || 0, String(currency))}\n          </Box>\n        </Box>\n        <Box>\n          <Box css={{ font: \"caption\", color: \"secondary\" }}>Pending</Box>\n          <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n            {formatCurrency(Number(pending) || 0, String(currency))}\n          </Box>\n        </Box>\n      </Box>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Chip Components\n// =========================================================================\nexport const Chip: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const { label, value, removable, action } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIChip\n      label={String(label || \"\")}\n      value={value ? String(value) : undefined}\n      onClose={removable && action ? () => emit(\"remove\") : undefined}\n    />\n  );\n};\n\nexport const ChipList: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIChipList>{children}</UIChipList>;\n};\n\n// =========================================================================\n// Tooltip Component\n// =========================================================================\nexport const Tooltip: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { content, placement = \"top\" } = element.props as Record<\n    string,\n    unknown\n  >;\n\n  // Tooltip needs a trigger element - use children if available\n  if (!children) {\n    return <Box>{String(content || \"\")}</Box>;\n  }\n\n  return (\n    <UITooltip\n      trigger={<Box>{children}</Box>}\n      placement={placement as \"top\" | \"bottom\" | \"left\" | \"right\"}\n    >\n      <Box>{String(content || \"\")}</Box>\n    </UITooltip>\n  );\n};\n\n// =========================================================================\n// Fallback Component\n// =========================================================================\nexport const Fallback: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  return (\n    <Box css={{ padding: \"small\", keyline: \"critical\", borderRadius: \"small\" }}>\n      <Box css={{ color: \"critical\", font: \"caption\" }}>\n        Unknown component: {element.type}\n      </Box>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Component Map Export\n// =========================================================================\nexport const components: Record<\n  string,\n  FunctionComponent<ExtendedRenderProps>\n> = {\n  // Layout\n  Stack,\n  Inline,\n  Divider,\n  Accordion,\n  AccordionItem,\n  // Typography\n  Heading,\n  Text,\n  // Data Display\n  Metric,\n  Badge,\n  Icon,\n  Img,\n  Spinner,\n  // Feedback\n  Banner,\n  // Lists\n  List,\n  ListItem,\n  PropertyList,\n  PropertyListItem,\n  TaskList,\n  TaskListItem,\n  // Menus\n  Menu,\n  MenuItem,\n  MenuGroup,\n  // Forms\n  TextField,\n  TextArea,\n  Select,\n  Checkbox,\n  Radio,\n  Switch,\n  DateField,\n  // Buttons\n  Button,\n  ButtonGroup,\n  Link,\n  // Charts\n  BarChart,\n  LineChart,\n  Sparkline,\n  // Tables\n  DataTable,\n  // Stripe Cards\n  CustomerCard,\n  PaymentCard,\n  SubscriptionCard,\n  InvoiceCard,\n  RefundCard,\n  DisputeCard,\n  BalanceCard,\n  // Chips\n  Chip,\n  ChipList,\n  // Tooltip\n  Tooltip,\n  // Fallback\n  Fallback,\n};\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\n/**\n * Stripe UIXT Catalog\n *\n * Comprehensive catalog mapping to Stripe UI Extension SDK components.\n */\nexport const stripeCatalog = defineCatalog(schema, {\n  components: {\n    // =========================================================================\n    // Layout Components\n    // =========================================================================\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).default(\"vertical\"),\n        gap: z\n          .enum([\"xsmall\", \"small\", \"medium\", \"large\", \"xlarge\"])\n          .default(\"medium\"),\n        alignX: z.enum([\"start\", \"center\", \"end\", \"stretch\"]).nullable(),\n        alignY: z\n          .enum([\"top\", \"center\", \"baseline\", \"bottom\", \"stretch\"])\n          .nullable(),\n        distribute: z.enum([\"space-between\", \"packed\"]).nullable(),\n      }),\n      description:\n        \"Flex layout container for arranging children horizontally or vertically with configurable gap and alignment\",\n      example: { direction: \"vertical\", gap: \"medium\" },\n    },\n\n    Inline: {\n      props: z.object({\n        gap: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"small\"),\n      }),\n      description:\n        \"Inline layout for text and small elements that wrap naturally\",\n    },\n\n    Divider: {\n      props: z.object({}),\n      description:\n        \"Visual horizontal divider line to separate content sections\",\n    },\n\n    Accordion: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description:\n        \"Collapsible accordion container for expandable content sections\",\n    },\n\n    AccordionItem: {\n      props: z.object({\n        title: z.string(),\n        subtitle: z.string().nullable(),\n        defaultOpen: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Individual accordion item with title, optional subtitle, and collapsible content\",\n    },\n\n    // =========================================================================\n    // Typography Components\n    // =========================================================================\n    Heading: {\n      props: z.object({\n        text: z.string(),\n        size: z\n          .enum([\"xsmall\", \"small\", \"medium\", \"large\", \"xlarge\"])\n          .default(\"large\"),\n      }),\n      description: \"Display a heading/title text with configurable size\",\n      example: { text: \"Overview\", size: \"large\" },\n    },\n\n    Text: {\n      props: z.object({\n        content: z.string(),\n        color: z\n          .enum([\n            \"primary\",\n            \"secondary\",\n            \"disabled\",\n            \"critical\",\n            \"success\",\n            \"warning\",\n            \"info\",\n          ])\n          .default(\"primary\"),\n        size: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"medium\"),\n        weight: z\n          .enum([\"regular\", \"medium\", \"semibold\", \"bold\"])\n          .default(\"regular\"),\n      }),\n      description:\n        \"Display body text with configurable color, size, and weight\",\n      example: { content: \"Payment received successfully.\", color: \"primary\" },\n    },\n\n    // =========================================================================\n    // Data Display Components\n    // =========================================================================\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        change: z.string().nullable(),\n        changeType: z\n          .enum([\"positive\", \"negative\", \"neutral\"])\n          .default(\"neutral\"),\n        format: z.enum([\"currency\", \"number\", \"percent\"]).nullable(),\n      }),\n      description:\n        \"Display a key metric with label, value, and optional trend indicator for KPIs\",\n      example: {\n        label: \"Revenue\",\n        value: \"$12,450\",\n        change: \"+8.2%\",\n        changeType: \"positive\",\n        format: \"currency\",\n      },\n    },\n\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        type: z\n          .enum([\n            \"neutral\",\n            \"urgent\",\n            \"warning\",\n            \"negative\",\n            \"positive\",\n            \"info\",\n          ])\n          .default(\"neutral\"),\n      }),\n      description: \"Status badge indicator with configurable color type\",\n      example: { label: \"Active\", type: \"positive\" },\n    },\n\n    Icon: {\n      props: z.object({\n        name: z.string(),\n        size: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"medium\"),\n        color: z\n          .enum([\n            \"primary\",\n            \"secondary\",\n            \"disabled\",\n            \"critical\",\n            \"success\",\n            \"warning\",\n            \"info\",\n          ])\n          .nullable(),\n      }),\n      description:\n        \"Display an icon from Stripe's icon set (e.g., 'check', 'warning', 'customer', 'payment')\",\n    },\n\n    Img: {\n      props: z.object({\n        src: z.string(),\n        alt: z.string().nullable(),\n        width: z.number().nullable(),\n        height: z.number().nullable(),\n      }),\n      description: \"Display an image with configurable dimensions\",\n    },\n\n    Spinner: {\n      props: z.object({\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n      }),\n      description: \"Loading spinner indicator\",\n    },\n\n    // =========================================================================\n    // Feedback Components\n    // =========================================================================\n    Banner: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n        type: z.enum([\"default\", \"caution\", \"critical\"]).default(\"default\"),\n        dismissible: z.boolean().nullable(),\n      }),\n      description:\n        \"Alert banner for important messages with optional dismiss button\",\n    },\n\n    // =========================================================================\n    // List Components\n    // =========================================================================\n    List: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description:\n        \"Container for ListItem components with optional action handling\",\n    },\n\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        secondaryTitle: z.string().nullable(),\n        value: z.string().nullable(),\n        id: z.string().nullable(),\n        size: z.enum([\"default\", \"large\"]).default(\"default\"),\n      }),\n      description:\n        \"List item with title, optional secondary text and value display\",\n    },\n\n    PropertyList: {\n      props: z.object({\n        orientation: z.enum([\"vertical\", \"horizontal\"]).default(\"vertical\"),\n      }),\n      slots: [\"default\"],\n      description:\n        \"List of label-value pairs for displaying properties/details\",\n    },\n\n    PropertyListItem: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n      }),\n      description: \"Single property item with label and value\",\n    },\n\n    TaskList: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Container for task items with status indicators\",\n    },\n\n    TaskListItem: {\n      props: z.object({\n        title: z.string(),\n        status: z\n          .enum([\"not-started\", \"in-progress\", \"blocked\", \"complete\"])\n          .default(\"not-started\"),\n        action: z.string().nullable(),\n      }),\n      description: \"Task item with title and status indicator\",\n    },\n\n    // =========================================================================\n    // Menu Components\n    // =========================================================================\n    Menu: {\n      props: z.object({\n        triggerLabel: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Dropdown menu container with trigger button\",\n    },\n\n    MenuItem: {\n      props: z.object({\n        label: z.string(),\n        id: z.string(),\n        disabled: z.boolean().nullable(),\n        action: z.string().nullable(),\n      }),\n      description: \"Menu item with label and optional action\",\n    },\n\n    MenuGroup: {\n      props: z.object({\n        title: z.string().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Group of menu items with optional title\",\n    },\n\n    // =========================================================================\n    // Form Components\n    // =========================================================================\n    TextField: {\n      props: z.object({\n        label: z.string(),\n        placeholder: z.string().nullable(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        type: z\n          .enum([\"text\", \"email\", \"password\", \"number\", \"tel\", \"url\"])\n          .default(\"text\"),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Text input field with label, validation. Use $bindState on value for two-way binding.\",\n      example: {\n        label: \"Email\",\n        placeholder: \"customer@example.com\",\n        value: { $bindState: \"/form/email\" },\n        type: \"email\",\n      },\n    },\n\n    TextArea: {\n      props: z.object({\n        label: z.string(),\n        placeholder: z.string().nullable(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        rows: z.number().default(3),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Multi-line text input with configurable rows. Use $bindState on value for two-way binding.\",\n    },\n\n    Select: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        options: z.array(z.object({ value: z.string(), label: z.string() })),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Dropdown select input with configurable options. Use $bindState on value for two-way binding.\",\n    },\n\n    Checkbox: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Checkbox input with label and description. Use $bindState on checked for two-way binding.\",\n    },\n\n    Radio: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        value: z.string().nullable(),\n        optionValue: z.string(),\n        name: z.string(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Radio button input for selecting one option from a group. Use $bindState on value for two-way binding.\",\n    },\n\n    Switch: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Toggle switch for boolean values. Use $bindState on checked for two-way binding.\",\n    },\n\n    DateField: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Date input field with date picker. Use $bindState on value for two-way binding.\",\n    },\n\n    // =========================================================================\n    // Button Components\n    // =========================================================================\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n        actionParams: z.record(z.string(), z.unknown()).nullable(),\n        type: z\n          .enum([\"primary\", \"secondary\", \"destructive\"])\n          .default(\"primary\"),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        pending: z.boolean().nullable(),\n        href: z.string().nullable(),\n      }),\n      description:\n        \"Action button with configurable style, size, and action handling\",\n      example: {\n        label: \"View Details\",\n        action: \"viewCustomer\",\n        type: \"primary\",\n      },\n    },\n\n    ButtonGroup: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Group of buttons displayed together\",\n    },\n\n    Link: {\n      props: z.object({\n        label: z.string(),\n        href: z.string(),\n        type: z.enum([\"primary\", \"secondary\"]).default(\"primary\"),\n        external: z.boolean().nullable(),\n      }),\n      description: \"Text link for navigation\",\n    },\n\n    // =========================================================================\n    // Chart Components\n    // =========================================================================\n    BarChart: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        colorKey: z.string().nullable(),\n        showAxis: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"both\"),\n        showGrid: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"none\"),\n        showLegend: z.boolean().nullable(),\n        showTooltip: z.boolean().default(true),\n      }),\n      description:\n        \"Bar chart visualization. Use $state on data to bind to state array.\",\n    },\n\n    LineChart: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        colorKey: z.string().nullable(),\n        showAxis: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"both\"),\n        showGrid: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"none\"),\n        showLegend: z.boolean().nullable(),\n        showTooltip: z.boolean().default(true),\n      }),\n      description:\n        \"Line chart visualization. Use $state on data to bind to state array.\",\n    },\n\n    Sparkline: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        showTooltip: z.boolean().nullable(),\n      }),\n      description:\n        \"Compact sparkline chart for inline data visualization. Use $state on data to bind to state array.\",\n    },\n\n    // =========================================================================\n    // Table Components\n    // =========================================================================\n    DataTable: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.unknown())).nullable(),\n        columns: z.array(z.object({ key: z.string(), label: z.string() })),\n        emptyMessage: z.string().nullable(),\n        rowAction: z.string().nullable(),\n      }),\n      description:\n        \"Data table with configurable columns and optional row actions. Use $state on data to bind to state array.\",\n      example: {\n        title: \"Recent Payments\",\n        data: { $state: \"/payments/data\" },\n        columns: [\n          { key: \"amount\", label: \"Amount\" },\n          { key: \"status\", label: \"Status\" },\n        ],\n      },\n    },\n\n    // =========================================================================\n    // Stripe-Specific Card Components\n    // =========================================================================\n    CustomerCard: {\n      props: z.object({\n        name: z.string(),\n        email: z.string(),\n        status: z.enum([\"active\", \"inactive\"]).default(\"active\"),\n        customerId: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying customer information with name, email, and status\",\n      example: {\n        name: \"Jane Smith\",\n        email: \"jane@example.com\",\n        status: \"active\",\n      },\n    },\n\n    PaymentCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\n            \"succeeded\",\n            \"pending\",\n            \"failed\",\n            \"canceled\",\n            \"requires_action\",\n          ])\n          .default(\"succeeded\"),\n        description: z.string().nullable(),\n        paymentId: z.string().nullable(),\n      }),\n      description: \"Card displaying payment information with amount and status\",\n    },\n\n    SubscriptionCard: {\n      props: z.object({\n        planName: z.string(),\n        status: z\n          .enum([\n            \"active\",\n            \"trialing\",\n            \"past_due\",\n            \"canceled\",\n            \"unpaid\",\n            \"incomplete\",\n          ])\n          .default(\"active\"),\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        interval: z.enum([\"day\", \"week\", \"month\", \"year\"]).default(\"month\"),\n        currentPeriodEnd: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying subscription details with plan, status, and billing info\",\n    },\n\n    InvoiceCard: {\n      props: z.object({\n        invoiceNumber: z.string(),\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\"draft\", \"open\", \"paid\", \"void\", \"uncollectible\"])\n          .default(\"open\"),\n        dueDate: z.string().nullable(),\n        customerEmail: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying invoice details with number, amount, and status\",\n    },\n\n    RefundCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\"pending\", \"succeeded\", \"failed\", \"canceled\"])\n          .default(\"succeeded\"),\n        reason: z.string().nullable(),\n      }),\n      description: \"Card displaying refund information\",\n    },\n\n    DisputeCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\n            \"warning_needs_response\",\n            \"warning_under_review\",\n            \"warning_closed\",\n            \"needs_response\",\n            \"under_review\",\n            \"won\",\n            \"lost\",\n          ])\n          .default(\"needs_response\"),\n        reason: z.string().nullable(),\n        dueDate: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying dispute information with status and deadline\",\n    },\n\n    BalanceCard: {\n      props: z.object({\n        available: z.number(),\n        pending: z.number(),\n        currency: z.string().default(\"usd\"),\n      }),\n      description: \"Card showing account balance breakdown\",\n    },\n\n    // =========================================================================\n    // Chip Components\n    // =========================================================================\n    Chip: {\n      props: z.object({\n        label: z.string(),\n        value: z.string().nullable(),\n        removable: z.boolean().nullable(),\n        action: z.string().nullable(),\n      }),\n      description: \"Chip/tag component for filters or selections\",\n    },\n\n    ChipList: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Container for multiple chips\",\n    },\n\n    // =========================================================================\n    // Tooltip Component\n    // =========================================================================\n    Tooltip: {\n      props: z.object({\n        content: z.string(),\n        placement: z.enum([\"top\", \"bottom\", \"left\", \"right\"]).default(\"top\"),\n      }),\n      slots: [\"default\"],\n      description: \"Tooltip that appears on hover over child element\",\n    },\n  },\n\n  actions: {\n    // =========================================================================\n    // Customer Actions\n    // =========================================================================\n    fetchCustomers: {\n      params: z.object({\n        limit: z.number().nullable(),\n        email: z.string().nullable(),\n        startingAfter: z.string().nullable(),\n      }),\n      description:\n        \"Fetch customers list with pagination. Data at '/customers/data'\",\n    },\n    viewCustomer: {\n      params: z.object({ customerId: z.string() }),\n      description: \"Open customer details in Stripe Dashboard\",\n    },\n    createCustomer: {\n      params: z.object({\n        email: z.string(),\n        name: z.string().nullable(),\n        phone: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new customer with optional metadata\",\n    },\n    updateCustomer: {\n      params: z.object({\n        customerId: z.string(),\n        email: z.string().nullable(),\n        name: z.string().nullable(),\n        phone: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update an existing customer\",\n    },\n    deleteCustomer: {\n      params: z.object({ customerId: z.string() }),\n      description: \"Delete a customer permanently\",\n    },\n    searchCustomers: {\n      params: z.object({ query: z.string(), limit: z.number().nullable() }),\n      description: \"Search customers by email, name, or metadata\",\n    },\n\n    // =========================================================================\n    // Payment Intent Actions\n    // =========================================================================\n    fetchPayments: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        status: z.string().nullable(),\n        startingAfter: z.string().nullable(),\n      }),\n      description: \"Fetch payment intents list. Data at '/payments/data'\",\n    },\n    viewPayment: {\n      params: z.object({ paymentId: z.string() }),\n      description: \"Open payment details in Stripe Dashboard\",\n    },\n    createPaymentIntent: {\n      params: z.object({\n        amount: z.number(),\n        currency: z.string(),\n        customerId: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new payment intent\",\n    },\n    capturePayment: {\n      params: z.object({\n        paymentId: z.string(),\n        amountToCapture: z.number().nullable(),\n      }),\n      description: \"Capture an authorized payment\",\n    },\n    cancelPayment: {\n      params: z.object({\n        paymentId: z.string(),\n        reason: z\n          .enum([\n            \"duplicate\",\n            \"fraudulent\",\n            \"requested_by_customer\",\n            \"abandoned\",\n          ])\n          .nullable(),\n      }),\n      description: \"Cancel a payment intent\",\n    },\n\n    // =========================================================================\n    // Refund Actions\n    // =========================================================================\n    fetchRefunds: {\n      params: z.object({\n        limit: z.number().nullable(),\n        paymentIntentId: z.string().nullable(),\n        chargeId: z.string().nullable(),\n      }),\n      description: \"Fetch refunds list. Data at '/refunds/data'\",\n    },\n    refundPayment: {\n      params: z.object({\n        paymentId: z.string(),\n        amount: z.number().nullable(),\n        reason: z\n          .enum([\"duplicate\", \"fraudulent\", \"requested_by_customer\"])\n          .nullable(),\n      }),\n      description: \"Create a refund for a payment\",\n    },\n    updateRefund: {\n      params: z.object({\n        refundId: z.string(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update refund metadata\",\n    },\n\n    // =========================================================================\n    // Charge Actions\n    // =========================================================================\n    fetchCharges: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        paymentIntentId: z.string().nullable(),\n      }),\n      description: \"Fetch charges list. Data at '/charges/data'\",\n    },\n    captureCharge: {\n      params: z.object({ chargeId: z.string(), amount: z.number().nullable() }),\n      description: \"Capture a previously authorized charge\",\n    },\n\n    // =========================================================================\n    // Subscription Actions\n    // =========================================================================\n    fetchSubscriptions: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\n            \"active\",\n            \"past_due\",\n            \"unpaid\",\n            \"canceled\",\n            \"incomplete\",\n            \"incomplete_expired\",\n            \"trialing\",\n            \"paused\",\n          ])\n          .nullable(),\n        customerId: z.string().nullable(),\n        priceId: z.string().nullable(),\n      }),\n      description: \"Fetch subscriptions. Data at '/subscriptions/data'\",\n    },\n    viewSubscription: {\n      params: z.object({ subscriptionId: z.string() }),\n      description: \"Open subscription details in Stripe Dashboard\",\n    },\n    createSubscription: {\n      params: z.object({\n        customerId: z.string(),\n        priceId: z.string(),\n        quantity: z.number().nullable(),\n        trialPeriodDays: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new subscription for a customer\",\n    },\n    updateSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        priceId: z.string().nullable(),\n        quantity: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a subscription (change price, quantity)\",\n    },\n    cancelSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        immediately: z.boolean().nullable(),\n        cancelAtPeriodEnd: z.boolean().nullable(),\n      }),\n      description: \"Cancel a subscription immediately or at period end\",\n    },\n    pauseSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        resumeAt: z.number().nullable(),\n      }),\n      description: \"Pause a subscription's payment collection\",\n    },\n    resumeSubscription: {\n      params: z.object({ subscriptionId: z.string() }),\n      description: \"Resume a paused subscription\",\n    },\n\n    // =========================================================================\n    // Invoice Actions\n    // =========================================================================\n    fetchInvoices: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\"draft\", \"open\", \"paid\", \"uncollectible\", \"void\"])\n          .nullable(),\n        customerId: z.string().nullable(),\n        subscriptionId: z.string().nullable(),\n      }),\n      description: \"Fetch invoices. Data at '/invoices/data'\",\n    },\n    viewInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Open invoice in Stripe Dashboard\",\n    },\n    createInvoice: {\n      params: z.object({\n        customerId: z.string(),\n        description: z.string().nullable(),\n        daysUntilDue: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a draft invoice for a customer\",\n    },\n    addInvoiceItem: {\n      params: z.object({\n        invoiceId: z.string(),\n        amount: z.number(),\n        currency: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Add a line item to an invoice\",\n    },\n    finalizeInvoice: {\n      params: z.object({\n        invoiceId: z.string(),\n        autoAdvance: z.boolean().nullable(),\n      }),\n      description: \"Finalize a draft invoice\",\n    },\n    sendInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Send invoice to customer via email\",\n    },\n    payInvoice: {\n      params: z.object({\n        invoiceId: z.string(),\n        paymentMethodId: z.string().nullable(),\n      }),\n      description: \"Pay an open invoice\",\n    },\n    voidInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Void an invoice (cannot be undone)\",\n    },\n    markInvoiceUncollectible: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Mark invoice as uncollectible\",\n    },\n    downloadInvoicePdf: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Download invoice as PDF\",\n    },\n\n    // =========================================================================\n    // Product & Price Actions\n    // =========================================================================\n    fetchProducts: {\n      params: z.object({\n        limit: z.number().nullable(),\n        active: z.boolean().nullable(),\n      }),\n      description: \"Fetch products list. Data at '/products/data'\",\n    },\n    viewProduct: {\n      params: z.object({ productId: z.string() }),\n      description: \"Open product in Stripe Dashboard\",\n    },\n    createProduct: {\n      params: z.object({\n        name: z.string(),\n        description: z.string().nullable(),\n        active: z.boolean().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new product\",\n    },\n    updateProduct: {\n      params: z.object({\n        productId: z.string(),\n        name: z.string().nullable(),\n        description: z.string().nullable(),\n        active: z.boolean().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a product\",\n    },\n    archiveProduct: {\n      params: z.object({ productId: z.string() }),\n      description: \"Archive a product (set active=false)\",\n    },\n    fetchPrices: {\n      params: z.object({\n        limit: z.number().nullable(),\n        productId: z.string().nullable(),\n        active: z.boolean().nullable(),\n        type: z.enum([\"one_time\", \"recurring\"]).nullable(),\n      }),\n      description: \"Fetch prices list. Data at '/prices/data'\",\n    },\n    createPrice: {\n      params: z.object({\n        productId: z.string(),\n        unitAmount: z.number(),\n        currency: z.string(),\n        recurring: z\n          .object({\n            interval: z.enum([\"day\", \"week\", \"month\", \"year\"]),\n            intervalCount: z.number().nullable(),\n          })\n          .nullable(),\n        nickname: z.string().nullable(),\n      }),\n      description: \"Create a new price for a product\",\n    },\n    updatePrice: {\n      params: z.object({\n        priceId: z.string(),\n        active: z.boolean().nullable(),\n        nickname: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a price\",\n    },\n\n    // =========================================================================\n    // Balance & Payout Actions\n    // =========================================================================\n    fetchBalance: {\n      params: z.object({}),\n      description: \"Fetch current account balance. Data at '/balance'\",\n    },\n    fetchPayouts: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z.enum([\"pending\", \"paid\", \"failed\", \"canceled\"]).nullable(),\n      }),\n      description: \"Fetch payouts list. Data at '/payouts/data'\",\n    },\n    createPayout: {\n      params: z.object({\n        amount: z.number(),\n        currency: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Create a payout to your bank account\",\n    },\n    cancelPayout: {\n      params: z.object({ payoutId: z.string() }),\n      description: \"Cancel a pending payout\",\n    },\n\n    // =========================================================================\n    // Dispute Actions\n    // =========================================================================\n    fetchDisputes: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\n            \"warning_needs_response\",\n            \"warning_under_review\",\n            \"warning_closed\",\n            \"needs_response\",\n            \"under_review\",\n            \"won\",\n            \"lost\",\n          ])\n          .nullable(),\n        chargeId: z.string().nullable(),\n      }),\n      description: \"Fetch disputes list. Data at '/disputes/data'\",\n    },\n    viewDispute: {\n      params: z.object({ disputeId: z.string() }),\n      description: \"Open dispute in Stripe Dashboard\",\n    },\n    updateDispute: {\n      params: z.object({\n        disputeId: z.string(),\n        evidence: z\n          .object({\n            customerName: z.string().nullable(),\n            customerEmailAddress: z.string().nullable(),\n            productDescription: z.string().nullable(),\n            uncategorizedText: z.string().nullable(),\n          })\n          .nullable(),\n        submit: z.boolean().nullable(),\n      }),\n      description: \"Update dispute evidence\",\n    },\n    closeDispute: {\n      params: z.object({ disputeId: z.string() }),\n      description: \"Close a dispute and accept it\",\n    },\n\n    // =========================================================================\n    // Payment Method Actions\n    // =========================================================================\n    fetchPaymentMethods: {\n      params: z.object({\n        customerId: z.string(),\n        type: z\n          .enum([\"card\", \"bank_account\", \"us_bank_account\", \"sepa_debit\"])\n          .nullable(),\n      }),\n      description:\n        \"Fetch customer payment methods. Data at '/paymentMethods/data'\",\n    },\n    attachPaymentMethod: {\n      params: z.object({ paymentMethodId: z.string(), customerId: z.string() }),\n      description: \"Attach a payment method to a customer\",\n    },\n    detachPaymentMethod: {\n      params: z.object({ paymentMethodId: z.string() }),\n      description: \"Detach a payment method from its customer\",\n    },\n    setDefaultPaymentMethod: {\n      params: z.object({ customerId: z.string(), paymentMethodId: z.string() }),\n      description: \"Set the default payment method for a customer\",\n    },\n\n    // =========================================================================\n    // Coupon & Promotion Actions\n    // =========================================================================\n    fetchCoupons: {\n      params: z.object({ limit: z.number().nullable() }),\n      description: \"Fetch coupons list. Data at '/coupons/data'\",\n    },\n    createCoupon: {\n      params: z.object({\n        percentOff: z.number().nullable(),\n        amountOff: z.number().nullable(),\n        currency: z.string().nullable(),\n        duration: z.enum([\"forever\", \"once\", \"repeating\"]),\n        durationInMonths: z.number().nullable(),\n        name: z.string().nullable(),\n        maxRedemptions: z.number().nullable(),\n      }),\n      description: \"Create a new coupon\",\n    },\n    deleteCoupon: {\n      params: z.object({ couponId: z.string() }),\n      description: \"Delete a coupon\",\n    },\n    fetchPromotionCodes: {\n      params: z.object({\n        limit: z.number().nullable(),\n        couponId: z.string().nullable(),\n        active: z.boolean().nullable(),\n      }),\n      description: \"Fetch promotion codes. Data at '/promotionCodes/data'\",\n    },\n    createPromotionCode: {\n      params: z.object({\n        couponId: z.string(),\n        code: z.string().nullable(),\n        maxRedemptions: z.number().nullable(),\n        expiresAt: z.number().nullable(),\n      }),\n      description: \"Create a promotion code for a coupon\",\n    },\n\n    // =========================================================================\n    // Checkout Session Actions\n    // =========================================================================\n    createCheckoutSession: {\n      params: z.object({\n        mode: z.enum([\"payment\", \"subscription\", \"setup\"]),\n        lineItems: z.array(\n          z.object({ priceId: z.string(), quantity: z.number() }),\n        ),\n        successUrl: z.string(),\n        cancelUrl: z.string(),\n        customerId: z.string().nullable(),\n      }),\n      description: \"Create a Checkout Session and get the URL\",\n    },\n    fetchCheckoutSessions: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        paymentIntentId: z.string().nullable(),\n      }),\n      description: \"Fetch checkout sessions. Data at '/checkoutSessions/data'\",\n    },\n    expireCheckoutSession: {\n      params: z.object({ sessionId: z.string() }),\n      description: \"Expire an open checkout session\",\n    },\n\n    // =========================================================================\n    // Billing Portal Actions\n    // =========================================================================\n    createBillingPortalSession: {\n      params: z.object({ customerId: z.string(), returnUrl: z.string() }),\n      description: \"Create a billing portal session for customer self-service\",\n    },\n\n    // =========================================================================\n    // Webhook & Event Actions\n    // =========================================================================\n    fetchEvents: {\n      params: z.object({\n        limit: z.number().nullable(),\n        type: z.string().nullable(),\n        createdGte: z.number().nullable(),\n        createdLte: z.number().nullable(),\n      }),\n      description: \"Fetch recent events/webhooks. Data at '/events/data'\",\n    },\n\n    // =========================================================================\n    // Setup Intent Actions\n    // =========================================================================\n    createSetupIntent: {\n      params: z.object({\n        customerId: z.string().nullable(),\n        paymentMethodTypes: z.array(z.string()).nullable(),\n        usage: z.enum([\"on_session\", \"off_session\"]).nullable(),\n      }),\n      description: \"Create a SetupIntent for saving payment methods\",\n    },\n    fetchSetupIntents: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n      }),\n      description: \"Fetch setup intents. Data at '/setupIntents/data'\",\n    },\n    cancelSetupIntent: {\n      params: z.object({ setupIntentId: z.string() }),\n      description: \"Cancel a setup intent\",\n    },\n\n    // =========================================================================\n    // Tax Rate Actions\n    // =========================================================================\n    fetchTaxRates: {\n      params: z.object({\n        limit: z.number().nullable(),\n        active: z.boolean().nullable(),\n        inclusive: z.boolean().nullable(),\n      }),\n      description: \"Fetch tax rates. Data at '/taxRates/data'\",\n    },\n    createTaxRate: {\n      params: z.object({\n        displayName: z.string(),\n        percentage: z.number(),\n        inclusive: z.boolean(),\n        jurisdiction: z.string().nullable(),\n        description: z.string().nullable(),\n      }),\n      description: \"Create a new tax rate\",\n    },\n\n    // =========================================================================\n    // Data & Refresh Actions\n    // =========================================================================\n    refreshData: {\n      params: z.object({}),\n      description: \"Refresh all dashboard data\",\n    },\n    refreshCustomers: {\n      params: z.object({}),\n      description: \"Refresh customers data only\",\n    },\n    refreshPayments: {\n      params: z.object({}),\n      description: \"Refresh payments data only\",\n    },\n    refreshSubscriptions: {\n      params: z.object({}),\n      description: \"Refresh subscriptions data only\",\n    },\n    refreshInvoices: {\n      params: z.object({}),\n      description: \"Refresh invoices data only\",\n    },\n    exportData: {\n      params: z.object({\n        format: z.enum([\"csv\", \"json\"]),\n        dataType: z.enum([\n          \"customers\",\n          \"payments\",\n          \"subscriptions\",\n          \"invoices\",\n        ]),\n      }),\n      description: \"Export data as CSV or JSON\",\n    },\n\n    // =========================================================================\n    // Navigation Actions\n    // =========================================================================\n    navigate: {\n      params: z.object({ path: z.string() }),\n      description: \"Navigate to a Dashboard page\",\n    },\n    openDashboard: {\n      params: z.object({\n        page: z\n          .enum([\n            \"home\",\n            \"payments\",\n            \"customers\",\n            \"products\",\n            \"subscriptions\",\n            \"invoices\",\n            \"connect\",\n            \"reports\",\n            \"developers\",\n          ])\n          .nullable(),\n      }),\n      description: \"Open Stripe Dashboard page in new tab\",\n    },\n    openExternalLink: {\n      params: z.object({ url: z.string() }),\n      description: \"Open an external URL in new tab\",\n    },\n\n    // =========================================================================\n    // Form Actions\n    // =========================================================================\n    submitForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Submit form data\",\n    },\n    resetForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Reset form to initial values\",\n    },\n    validateForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Validate form without submitting\",\n    },\n    setFormValue: {\n      params: z.object({ path: z.string(), value: z.unknown() }),\n      description: \"Set a specific form field value\",\n    },\n\n    // =========================================================================\n    // UI Actions\n    // =========================================================================\n    showToast: {\n      params: z.object({\n        message: z.string(),\n        type: z.enum([\"success\", \"error\", \"warning\", \"info\"]).nullable(),\n      }),\n      description: \"Show a toast notification\",\n    },\n    copyToClipboard: {\n      params: z.object({ text: z.string() }),\n      description: \"Copy text to clipboard\",\n    },\n    setLoading: {\n      params: z.object({\n        loading: z.boolean(),\n        message: z.string().nullable(),\n      }),\n      description: \"Show/hide loading state\",\n    },\n\n    // =========================================================================\n    // Filter & Sort Actions\n    // =========================================================================\n    setFilter: {\n      params: z.object({ key: z.string(), value: z.unknown() }),\n      description: \"Set a filter value\",\n    },\n    clearFilters: {\n      params: z.object({}),\n      description: \"Clear all filters\",\n    },\n    setSort: {\n      params: z.object({\n        field: z.string(),\n        direction: z.enum([\"asc\", \"desc\"]),\n      }),\n      description: \"Set sort field and direction\",\n    },\n    setPageSize: {\n      params: z.object({ size: z.number() }),\n      description: \"Set items per page\",\n    },\n    goToPage: {\n      params: z.object({ page: z.number() }),\n      description: \"Navigate to a specific page\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/render/index.ts",
    "content": "// Catalog\nexport { stripeCatalog } from \"./catalog\";\n\n// Components\nexport { components, Fallback } from \"./catalog/components\";\n\n// Actions\nexport { actionHandlers, executeAction } from \"./catalog/actions\";\n\n// Renderer\nexport { StripeRenderer, type StripeRendererProps } from \"./renderer\";\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/render/renderer.tsx",
    "content": "import { useMemo, useRef, type ReactNode } from \"react\";\nimport {\n  Renderer,\n  type ComponentRegistry,\n  type Spec,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n} from \"@json-render/react\";\n\nimport { components, Fallback } from \"./catalog/components\";\nimport { executeAction } from \"./catalog/actions\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\nexport interface StripeRendererProps {\n  /** The UI spec to render (from json-render) */\n  spec: Spec | null;\n  /** Data context for components */\n  data?: Record<string, unknown>;\n  /** Function to update data */\n  setData?: SetState;\n  /** Callback when data changes */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n}\n\n// =============================================================================\n// Build Registry\n// =============================================================================\n\n/**\n * Build a component registry from our components map.\n * Uses refs to avoid recreating on data changes.\n */\nfunction buildRegistry(\n  dataRef: React.RefObject<Record<string, unknown>>,\n  setDataRef: React.RefObject<SetState | undefined>,\n  loading?: boolean,\n): ComponentRegistry {\n  const registry: ComponentRegistry = {};\n\n  for (const [name, Component] of Object.entries(components)) {\n    const noop = () => {};\n    registry[name] = (renderProps: {\n      element: { type: string; props: Record<string, unknown> };\n      children?: ReactNode;\n      emit?: (event: string) => void;\n    }) =>\n      Component({\n        element: renderProps.element,\n        children: renderProps.children,\n        emit: renderProps.emit ?? noop,\n        loading,\n        state: dataRef.current,\n        getValue: (path: string) => {\n          const data = dataRef.current;\n          const parts = path.replace(/^\\//, \"\").split(\"/\");\n          let current: unknown = data;\n          for (const part of parts) {\n            if (current && typeof current === \"object\" && part in current) {\n              current = (current as Record<string, unknown>)[part];\n            } else {\n              return undefined;\n            }\n          }\n          return current;\n        },\n        onAction: (actionName: string, params?: Record<string, unknown>) => {\n          const setState = setDataRef.current;\n          if (setState) {\n            executeAction(actionName, params, setState, dataRef.current);\n          }\n        },\n      });\n  }\n\n  return registry;\n}\n\n/**\n * Fallback component for unknown types\n */\nconst fallbackRegistry = (renderProps: {\n  element: { type: string; props: Record<string, unknown> };\n}) => <Fallback element={renderProps.element} />;\n\n// =============================================================================\n// StripeRenderer Component\n// =============================================================================\n\n/**\n * Main renderer component for Stripe UIXT.\n *\n * Wraps the json-render Renderer with all necessary providers\n * and connects it to the Stripe component implementations.\n */\nexport function StripeRenderer({\n  spec,\n  data = {},\n  setData,\n  onStateChange,\n  loading,\n}: StripeRendererProps): ReactNode {\n  // Use refs to keep registry stable while still accessing latest data/setData\n  const dataRef = useRef(data);\n  const setDataRef = useRef(setData);\n  dataRef.current = data;\n  setDataRef.current = setData;\n\n  // Memoize registry - only changes when loading changes\n  const registry = useMemo(\n    () => buildRegistry(dataRef, setDataRef, loading),\n    [loading],\n  );\n\n  const mergedState = useMemo(\n    () => (spec?.state ? { ...data, ...spec.state } : data),\n    [data, spec?.state],\n  );\n\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={mergedState} onStateChange={onStateChange}>\n      <VisibilityProvider>\n        <ActionProvider>\n          <Renderer\n            spec={spec}\n            registry={registry}\n            fallback={fallbackRegistry}\n            loading={loading}\n          />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/stream-spec.ts",
    "content": "import type { Spec } from \"@json-render/react\";\n\ninterface JsonPatch {\n  op: string;\n  path: string;\n  value?: unknown;\n  from?: string;\n}\n\nfunction setDeep(\n  obj: Record<string, unknown>,\n  segments: string[],\n  value: unknown,\n): void {\n  let current: unknown = obj;\n  for (let i = 0; i < segments.length - 1; i++) {\n    const seg = segments[i];\n    const next = (current as Record<string, unknown>)[seg];\n    if (next && typeof next === \"object\") {\n      current = next;\n    } else {\n      const container = /^\\d+$/.test(segments[i + 1]) ? [] : {};\n      (current as Record<string, unknown>)[seg] = container;\n      current = container;\n    }\n  }\n  const last = segments[segments.length - 1];\n  if (Array.isArray(current)) {\n    (current as unknown[])[Number(last)] = value;\n  } else {\n    (current as Record<string, unknown>)[last] = value;\n  }\n}\n\nfunction setSpecValue(spec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    (spec as Record<string, unknown>).root = value as string;\n    return;\n  }\n  if (path === \"/state\") {\n    (spec as Record<string, unknown>).state = value;\n    return;\n  }\n  if (path.startsWith(\"/state/\")) {\n    if (!spec.state) (spec as Record<string, unknown>).state = {};\n    const segments = path.slice(\"/state/\".length).split(\"/\");\n    setDeep(spec.state as Record<string, unknown>, segments, value);\n    return;\n  }\n  const elemMatch = path.match(/^\\/elements\\/(.+)/);\n  if (elemMatch) {\n    spec.elements[elemMatch[1]] = value as Spec[\"elements\"][string];\n  }\n}\n\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const next: Spec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\":\n      setSpecValue(next, patch.path, patch.value);\n      break;\n    case \"remove\":\n      if (patch.path.startsWith(\"/elements/\")) {\n        const key = patch.path.slice(\"/elements/\".length);\n        delete next.elements[key];\n      }\n      break;\n  }\n  return next;\n}\n\n/**\n * Fetch an AI generation endpoint that returns a JSONL patch stream,\n * applying patches progressively via onPatch.\n */\nexport async function streamSpec(\n  url: string,\n  body: Record<string, unknown>,\n  onPatch: (spec: Spec) => void,\n): Promise<Spec> {\n  const response = await fetch(url, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(body),\n  });\n\n  if (!response.ok) {\n    let msg = `API error: ${response.status}`;\n    try {\n      const errData = await response.json();\n      if (errData.error) msg = errData.error;\n    } catch {\n      // use default\n    }\n    throw new Error(msg);\n  }\n\n  const reader = response.body?.getReader();\n  if (!reader) throw new Error(\"No response body\");\n\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n  let spec: Spec = { root: \"\", elements: {} };\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    buffer += decoder.decode(value, { stream: true });\n    const lines = buffer.split(\"\\n\");\n    buffer = lines.pop() ?? \"\";\n\n    for (const line of lines) {\n      const trimmed = line.trim();\n      if (!trimmed || trimmed.startsWith(\"//\")) continue;\n      try {\n        const parsed = JSON.parse(trimmed) as JsonPatch;\n        if (parsed.op) {\n          spec = applyPatch(spec, parsed);\n          onPatch(spec);\n        }\n      } catch {\n        // skip non-JSON lines\n      }\n    }\n  }\n\n  if (buffer.trim()) {\n    try {\n      const parsed = JSON.parse(buffer.trim()) as JsonPatch;\n      if (parsed.op) {\n        spec = applyPatch(spec, parsed);\n        onPatch(spec);\n      }\n    } catch {\n      // skip\n    }\n  }\n\n  return spec;\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/lib/stripe.ts",
    "content": "import Stripe from \"stripe\";\nimport {\n  createHttpClient,\n  STRIPE_API_KEY,\n} from \"@stripe/ui-extension-sdk/http_client\";\n\n/**\n * Create a Stripe client for use in Stripe Apps.\n *\n * The Stripe UI Extension SDK handles authentication automatically\n * through the httpClient - no real API key is needed.\n */\nexport const stripe = new Stripe(STRIPE_API_KEY, {\n  httpClient: createHttpClient(),\n  apiVersion: \"2024-12-18.acacia\",\n});\n\n/**\n * Format amount for display (converts cents to dollars)\n */\nexport function formatAmount(amount: number, currency = \"usd\"): string {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: currency.toUpperCase(),\n  }).format(amount / 100);\n}\n\n/**\n * Format date for display\n */\nexport function formatDate(timestamp: number): string {\n  return new Date(timestamp * 1000).toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  });\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/CustomerDetails.test.tsx",
    "content": "import { render, getMockContextProps } from \"@stripe/ui-extension-sdk/testing\";\nimport { ContextView } from \"@stripe/ui-extension-sdk/ui\";\n\nimport CustomerDetails from \"./CustomerDetails\";\n\ndescribe(\"CustomerDetailsView\", () => {\n  it(\"renders ContextView\", () => {\n    const { wrapper } = render(<CustomerDetails {...getMockContextProps()} />);\n\n    expect(wrapper.find(ContextView)).toContainText(\"save to reload this view\");\n  });\n});\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/CustomerDetails.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createCustomerDetailSpec(\n  data: Record<string, unknown>,\n  customerId: string,\n): Spec {\n  const customers = data.customers as\n    | {\n        data?: Array<{\n          id: string;\n          name: string;\n          email: string;\n          phone?: string;\n          status: string;\n          created: string;\n          balance?: number;\n          currency?: string;\n        }>;\n      }\n    | undefined;\n\n  const customer = customers?.data?.find((c) => c.id === customerId);\n\n  const payments = data.customerPayments as\n    | {\n        data?: Array<{\n          id: string;\n          formattedAmount: string;\n          status: string;\n          description: string;\n          created: string;\n        }>;\n        total?: number;\n      }\n    | undefined;\n\n  const subscriptions = data.customerSubscriptions as\n    | {\n        data?: Array<{\n          id: string;\n          planName: string;\n          status: string;\n          amount: number;\n          interval: string;\n          currentPeriodEnd: string;\n        }>;\n        total?: number;\n      }\n    | undefined;\n\n  const invoices = data.customerInvoices as\n    | {\n        data?: Array<{\n          id: string;\n          invoiceNumber: string;\n          amount: number;\n          status: string;\n          dueDate: string;\n        }>;\n        total?: number;\n      }\n    | undefined;\n\n  const paymentsList = payments?.data ?? [];\n  const subscriptionsList = subscriptions?.data ?? [];\n  const invoicesList = invoices?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"header\", \"details\", \"sections\"],\n    },\n    header: {\n      type: \"Stack\",\n      props: {\n        direction: \"horizontal\",\n        gap: \"medium\",\n        distribute: \"space-between\",\n      },\n      children: [\"customerInfo\", \"actions\"],\n    },\n    customerInfo: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"xsmall\" },\n      children: [\"customerName\", \"customerEmail\"],\n    },\n    customerName: {\n      type: \"Heading\",\n      props: { text: customer?.name ?? \"Unknown Customer\", size: \"xlarge\" },\n      children: [],\n    },\n    customerEmail: {\n      type: \"Text\",\n      props: { content: customer?.email ?? \"\", color: \"secondary\" },\n      children: [],\n    },\n    actions: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"editBtn\", \"portalBtn\"],\n    },\n    editBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Edit\",\n        action: \"viewCustomer\",\n        actionParams: { customerId },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    portalBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Billing Portal\",\n        action: \"createBillingPortalSession\",\n        actionParams: { customerId, returnUrl: window.location.href },\n        type: \"primary\",\n      },\n      children: [],\n    },\n    details: {\n      type: \"PropertyList\",\n      props: { orientation: \"horizontal\" },\n      children: [\"detailStatus\", \"detailCreated\", \"detailBalance\"],\n    },\n    detailStatus: {\n      type: \"PropertyListItem\",\n      props: { label: \"Status\", value: customer?.status ?? \"Unknown\" },\n      children: [],\n    },\n    detailCreated: {\n      type: \"PropertyListItem\",\n      props: { label: \"Created\", value: customer?.created ?? \"Unknown\" },\n      children: [],\n    },\n    detailBalance: {\n      type: \"PropertyListItem\",\n      props: {\n        label: \"Balance\",\n        value: customer?.balance\n          ? `$${(customer.balance / 100).toFixed(2)}`\n          : \"$0.00\",\n      },\n      children: [],\n    },\n    sections: {\n      type: \"Accordion\",\n      props: {},\n      children: [\"paymentsSection\", \"subscriptionsSection\", \"invoicesSection\"],\n    },\n\n    // Payments Section\n    paymentsSection: {\n      type: \"AccordionItem\",\n      props: { title: `Payments (${payments?.total ?? 0})`, defaultOpen: true },\n      children: paymentsList.length > 0 ? [\"paymentsList\"] : [\"noPayments\"],\n    },\n    paymentsList: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: paymentsList.slice(0, 5).map((_, i) => `payment${i}`),\n    },\n    noPayments: {\n      type: \"Text\",\n      props: { content: \"No payments found\", color: \"secondary\" },\n      children: [],\n    },\n\n    // Subscriptions Section\n    subscriptionsSection: {\n      type: \"AccordionItem\",\n      props: {\n        title: `Subscriptions (${subscriptions?.total ?? 0})`,\n        defaultOpen: false,\n      },\n      children:\n        subscriptionsList.length > 0\n          ? [\"subscriptionsList\"]\n          : [\"noSubscriptions\"],\n    },\n    subscriptionsList: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: subscriptionsList.slice(0, 5).map((_, i) => `subscription${i}`),\n    },\n    noSubscriptions: {\n      type: \"Text\",\n      props: { content: \"No subscriptions found\", color: \"secondary\" },\n      children: [],\n    },\n\n    // Invoices Section\n    invoicesSection: {\n      type: \"AccordionItem\",\n      props: {\n        title: `Invoices (${invoices?.total ?? 0})`,\n        defaultOpen: false,\n      },\n      children: invoicesList.length > 0 ? [\"invoicesList\"] : [\"noInvoices\"],\n    },\n    invoicesList: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: invoicesList.slice(0, 5).map((_, i) => `invoice${i}`),\n    },\n    noInvoices: {\n      type: \"Text\",\n      props: { content: \"No invoices found\", color: \"secondary\" },\n      children: [],\n    },\n  };\n\n  // Add payment cards\n  paymentsList.slice(0, 5).forEach((p, i) => {\n    elements[`payment${i}`] = {\n      type: \"PaymentCard\",\n      props: {\n        amount: 0,\n        currency: \"usd\",\n        status: p.status as \"succeeded\" | \"pending\" | \"failed\",\n        description: `${p.formattedAmount} - ${p.description}`,\n        paymentId: p.id,\n      },\n      children: [],\n    };\n  });\n\n  // Add subscription cards\n  subscriptionsList.slice(0, 5).forEach((s, i) => {\n    elements[`subscription${i}`] = {\n      type: \"SubscriptionCard\",\n      props: {\n        planName: s.planName,\n        status: s.status as \"active\" | \"trialing\" | \"past_due\" | \"canceled\",\n        amount: s.amount,\n        currency: \"usd\",\n        interval: s.interval as \"month\" | \"year\",\n        currentPeriodEnd: s.currentPeriodEnd,\n      },\n      children: [],\n    };\n  });\n\n  // Add invoice cards\n  invoicesList.slice(0, 5).forEach((inv, i) => {\n    elements[`invoice${i}`] = {\n      type: \"InvoiceCard\",\n      props: {\n        invoiceNumber: inv.invoiceNumber,\n        amount: inv.amount,\n        currency: \"usd\",\n        status: inv.status as \"draft\" | \"open\" | \"paid\" | \"void\",\n        dueDate: inv.dueDate,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst CustomerDetails = ({ environment }: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  // Get customer ID from context\n  const customerId = environment?.objectContext?.id ?? \"\";\n\n  // Wrap setState for action handlers\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  // Load customer data\n  useEffect(() => {\n    const loadData = async () => {\n      if (!customerId) {\n        setLoading(false);\n        return;\n      }\n\n      setLoading(true);\n\n      // Fetch customer and related data\n      await Promise.all([\n        executeAction(\n          \"fetchCustomers\",\n          { email: null, limit: 100 },\n          handleSetState,\n          {},\n        ),\n        executeAction(\n          \"fetchPayments\",\n          { customerId, limit: 10 },\n          (updater) => {\n            setState((prev) => {\n              const next = updater(prev);\n              return { ...prev, customerPayments: next.payments };\n            });\n          },\n          {},\n        ),\n        executeAction(\n          \"fetchSubscriptions\",\n          { customerId, limit: 10 },\n          (updater) => {\n            setState((prev) => {\n              const next = updater(prev);\n              return { ...prev, customerSubscriptions: next.subscriptions };\n            });\n          },\n          {},\n        ),\n        executeAction(\n          \"fetchInvoices\",\n          { customerId, limit: 10 },\n          (updater) => {\n            setState((prev) => {\n              const next = updater(prev);\n              return { ...prev, customerInvoices: next.invoices };\n            });\n          },\n          {},\n        ),\n      ]);\n\n      setLoading(false);\n    };\n    loadData();\n  }, [customerId, handleSetState]);\n\n  // Update spec when data changes\n  useEffect(() => {\n    if (!loading && customerId) {\n      setSpec(createCustomerDetailSpec(data, customerId));\n    }\n  }, [data, loading, customerId]);\n\n  // Generate custom UI\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          prompt: `For customer ${customerId}: ${prompt}`,\n          systemPrompt,\n        }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createCustomerDetailSpec(data, customerId));\n    }\n\n    setGenerating(false);\n  };\n\n  if (!customerId) {\n    return (\n      <ContextView\n        title=\"Customer Details\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box css={{ color: \"secondary\", padding: \"large\" }}>\n          No customer selected. Please select a customer from the list.\n        </Box>\n      </ContextView>\n    );\n  }\n\n  if (loading) {\n    return (\n      <ContextView\n        title=\"Customer Details\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading customer details...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Customer Details\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\"viewCustomer\", { customerId }, handleSetState, data)\n          }\n        >\n          View in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        {/* AI Generation Input */}\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the customer view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {/* Rendered Spec */}\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default CustomerDetails;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Customers.test.tsx",
    "content": "import { render, getMockContextProps } from \"@stripe/ui-extension-sdk/testing\";\nimport { ContextView } from \"@stripe/ui-extension-sdk/ui\";\n\nimport Customers from \"./Customers\";\n\ndescribe(\"CustomersView\", () => {\n  it(\"renders ContextView\", () => {\n    const { wrapper } = render(<Customers {...getMockContextProps()} />);\n\n    expect(wrapper.find(ContextView)).toContainText(\"save to reload this view\");\n  });\n});\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Customers.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createCustomersListSpec(data: Record<string, unknown>): Spec {\n  const customers = data.customers as\n    | {\n        data?: Array<{\n          id: string;\n          name: string;\n          email: string;\n          status: string;\n          created: string;\n        }>;\n        total?: number;\n        hasMore?: boolean;\n      }\n    | undefined;\n\n  const customerList = customers?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"stats\", \"actions\", \"list\", \"pagination\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Customer Directory\", size: \"xlarge\" },\n      children: [],\n    },\n    stats: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"totalMetric\"],\n    },\n    totalMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Customers\",\n        value: String(customers?.total ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    actions: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"refreshBtn\", \"exportBtn\"],\n    },\n    refreshBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh\",\n        action: \"refreshCustomers\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    exportBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Export CSV\",\n        action: \"exportData\",\n        actionParams: { format: \"csv\", dataType: \"customers\" },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    list: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: customerList.slice(0, 10).map((_, i) => `customer${i}`),\n    },\n    pagination: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: customers?.hasMore ? [\"loadMore\"] : [],\n    },\n    loadMore: {\n      type: \"Button\",\n      props: {\n        label: \"Load More\",\n        action: \"fetchCustomers\",\n        actionParams: {\n          limit: 10,\n          startingAfter: customerList[customerList.length - 1]?.id,\n        },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  customerList.slice(0, 10).forEach((c, i) => {\n    elements[`customer${i}`] = {\n      type: \"CustomerCard\",\n      props: {\n        name: c.name,\n        email: c.email,\n        status: c.status as \"active\" | \"inactive\",\n        customerId: c.id,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst Customers = (_props: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  // Wrap setState for action handlers\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => {\n        const next = updater(prev);\n        return next;\n      });\n    },\n    [],\n  );\n\n  // Load initial data\n  useEffect(() => {\n    const loadData = async () => {\n      setLoading(true);\n      await executeAction(\"fetchCustomers\", { limit: 10 }, handleSetState, {});\n      setLoading(false);\n    };\n    loadData();\n  }, [handleSetState]);\n\n  // Update spec when data changes\n  useEffect(() => {\n    if (!loading && Object.keys(data).length > 0) {\n      setSpec(createCustomersListSpec(data));\n    }\n  }, [data, loading]);\n\n  // Generate custom UI\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    // Use catalog prompt for system context\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, systemPrompt }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      // Fallback to data-driven spec\n      setSpec(createCustomersListSpec(data));\n    }\n\n    setGenerating(false);\n  };\n\n  if (loading) {\n    return (\n      <ContextView\n        title=\"Customer Directory\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading customers...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Customer Directory\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\n              \"openDashboard\",\n              { page: \"customers\" },\n              handleSetState,\n              data,\n            )\n          }\n        >\n          Open in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        {/* AI Generation Input */}\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the customer view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {/* Rendered Spec */}\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default Customers;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Home.test.tsx",
    "content": "import { render, getMockContextProps } from \"@stripe/ui-extension-sdk/testing\";\nimport { ContextView } from \"@stripe/ui-extension-sdk/ui\";\n\nimport Home from \"./Home\";\n\ndescribe(\"DashboardHomepageView\", () => {\n  it(\"renders ContextView\", () => {\n    const { wrapper } = render(<Home {...getMockContextProps()} />);\n\n    expect(wrapper.find(ContextView)).toContainText(\"save to reload this view\");\n  });\n});\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Home.tsx",
    "content": "import { useState, useCallback, useEffect } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Banner,\n  Box,\n  ContextView,\n  Divider,\n  Button,\n  Link,\n  TextField,\n  Tabs,\n  TabList,\n  Tab,\n  TabPanels,\n  TabPanel,\n  List,\n  ListItem,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { streamSpec } from \"../lib/stream-spec\";\n\n// =============================================================================\n// Dynamic Specs (use real data from context)\n// =============================================================================\n\nfunction createRevenueSpec(data: Record<string, unknown>): Spec {\n  const customers = data.customers as { total?: number } | undefined;\n  const payments = data.payments as\n    | { totalVolume?: string; successRate?: string }\n    | undefined;\n  const subscriptions = data.subscriptions as\n    | { active?: number; trialing?: number; pastDue?: number }\n    | undefined;\n\n  return {\n    root: \"root\",\n    elements: {\n      root: {\n        type: \"Stack\",\n        props: { direction: \"vertical\", gap: \"large\" },\n        children: [\"heading\", \"metrics\", \"refresh\"],\n      },\n      heading: {\n        type: \"Heading\",\n        props: { text: \"Revenue Dashboard\", size: \"xlarge\" },\n        children: [],\n      },\n      metrics: {\n        type: \"Stack\",\n        props: { direction: \"horizontal\", gap: \"medium\" },\n        children: [\"m1\", \"m2\", \"m3\"],\n      },\n      m1: {\n        type: \"Metric\",\n        props: {\n          label: \"Payment Volume\",\n          value: payments?.totalVolume ?? \"$0\",\n          change: payments?.successRate\n            ? `${payments.successRate} success`\n            : null,\n          changeType: \"positive\",\n        },\n        children: [],\n      },\n      m2: {\n        type: \"Metric\",\n        props: {\n          label: \"Active Subscriptions\",\n          value: String(subscriptions?.active ?? 0),\n          change: subscriptions?.trialing\n            ? `+${subscriptions.trialing} trialing`\n            : null,\n          changeType: \"positive\",\n        },\n        children: [],\n      },\n      m3: {\n        type: \"Metric\",\n        props: {\n          label: \"Total Customers\",\n          value: String(customers?.total ?? 0),\n          changeType: \"neutral\",\n        },\n        children: [],\n      },\n      refresh: {\n        type: \"Button\",\n        props: {\n          label: \"Refresh Data\",\n          action: \"refreshData\",\n          type: \"secondary\",\n        },\n        children: [],\n      },\n    },\n  };\n}\n\nfunction createPaymentsSpec(data: Record<string, unknown>): Spec {\n  const payments = data.payments as\n    | {\n        data?: Array<{\n          id: string;\n          formattedAmount: string;\n          status: string;\n          description: string;\n        }>;\n        totalVolume?: string;\n        successRate?: string;\n      }\n    | undefined;\n  const paymentsList = payments?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"stats\", \"payments\", \"refresh\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Recent Payments\", size: \"xlarge\" },\n      children: [],\n    },\n    stats: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"s1\", \"s2\"],\n    },\n    s1: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Volume\",\n        value: payments?.totalVolume ?? \"$0\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    s2: {\n      type: \"Metric\",\n      props: {\n        label: \"Success Rate\",\n        value: payments?.successRate ?? \"0%\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    payments: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: paymentsList.slice(0, 5).map((_, i) => `p${i}`),\n    },\n    refresh: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh Payments\",\n        action: \"fetchPayments\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  paymentsList.slice(0, 5).forEach((p, i) => {\n    elements[`p${i}`] = {\n      type: \"PaymentCard\",\n      props: {\n        amount: 0, // We'll use formattedAmount in description\n        currency: \"usd\",\n        status: p.status as \"succeeded\" | \"pending\" | \"failed\",\n        description: `${p.formattedAmount} - ${p.description}`,\n        paymentId: p.id,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\nfunction createSubscriptionsSpec(data: Record<string, unknown>): Spec {\n  const subscriptions = data.subscriptions as\n    | {\n        data?: Array<{\n          id: string;\n          planName: string;\n          status: string;\n          amount: number;\n          interval: string;\n        }>;\n        active?: number;\n        trialing?: number;\n        pastDue?: number;\n      }\n    | undefined;\n  const subsList = subscriptions?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"metrics\", \"alert\", \"subs\", \"refresh\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Subscriptions Overview\", size: \"xlarge\" },\n      children: [],\n    },\n    metrics: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"m1\", \"m2\", \"m3\"],\n    },\n    m1: {\n      type: \"Metric\",\n      props: {\n        label: \"Active\",\n        value: String(subscriptions?.active ?? 0),\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    m2: {\n      type: \"Metric\",\n      props: {\n        label: \"Trialing\",\n        value: String(subscriptions?.trialing ?? 0),\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    m3: {\n      type: \"Metric\",\n      props: {\n        label: \"Past Due\",\n        value: String(subscriptions?.pastDue ?? 0),\n        changeType: subscriptions?.pastDue ? \"negative\" : \"neutral\",\n      },\n      children: [],\n    },\n    alert: {\n      type: \"Banner\",\n      props: {\n        title: `${subscriptions?.trialing ?? 0} subscriptions currently trialing`,\n        type: \"default\",\n      },\n      children: [],\n    },\n    subs: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: subsList.slice(0, 5).map((_, i) => `sub${i}`),\n    },\n    refresh: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh Subscriptions\",\n        action: \"fetchSubscriptions\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  subsList.slice(0, 5).forEach((s, i) => {\n    elements[`sub${i}`] = {\n      type: \"SubscriptionCard\",\n      props: {\n        planName: s.planName,\n        status: s.status as \"active\" | \"trialing\" | \"past_due\" | \"canceled\",\n        amount: s.amount,\n        interval: (s.interval as \"month\" | \"year\") ?? \"month\",\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\nfunction createCustomersSpec(data: Record<string, unknown>): Spec {\n  const customers = data.customers as\n    | {\n        data?: Array<{\n          id: string;\n          name: string;\n          email: string;\n          status: string;\n        }>;\n        total?: number;\n      }\n    | undefined;\n  const customersList = customers?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"stats\", \"customers\", \"refresh\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Customer Directory\", size: \"xlarge\" },\n      children: [],\n    },\n    stats: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"s1\"],\n    },\n    s1: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Customers\",\n        value: String(customers?.total ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    customers: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: customersList.slice(0, 5).map((_, i) => `c${i}`),\n    },\n    refresh: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh Customers\",\n        action: \"fetchCustomers\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  customersList.slice(0, 5).forEach((c, i) => {\n    elements[`c${i}`] = {\n      type: \"CustomerCard\",\n      props: {\n        name: c.name,\n        email: c.email,\n        status: c.status as \"active\" | \"inactive\",\n        customerId: c.id,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\nfunction createInvoicesSpec(data: Record<string, unknown>): Spec {\n  const invoices = data.invoices as\n    | {\n        data?: Array<{\n          id: string;\n          invoiceNumber: string;\n          amount: number;\n          status: string;\n          dueDate: string | null;\n        }>;\n        outstanding?: string;\n        paid?: string;\n        overdue?: string;\n      }\n    | undefined;\n  const invoicesList = invoices?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"stats\", \"invoices\", \"refresh\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoice Management\", size: \"xlarge\" },\n      children: [],\n    },\n    stats: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"s1\", \"s2\", \"s3\"],\n    },\n    s1: {\n      type: \"Metric\",\n      props: {\n        label: \"Outstanding\",\n        value: invoices?.outstanding ?? \"$0\",\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    s2: {\n      type: \"Metric\",\n      props: {\n        label: \"Paid\",\n        value: invoices?.paid ?? \"$0\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    s3: {\n      type: \"Metric\",\n      props: {\n        label: \"Overdue\",\n        value: invoices?.overdue ?? \"$0\",\n        changeType: invoices?.overdue !== \"$0.00\" ? \"negative\" : \"neutral\",\n      },\n      children: [],\n    },\n    invoices: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: invoicesList.slice(0, 5).map((_, i) => `inv${i}`),\n    },\n    refresh: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh Invoices\",\n        action: \"fetchInvoices\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  invoicesList.slice(0, 5).forEach((inv, i) => {\n    elements[`inv${i}`] = {\n      type: \"InvoiceCard\",\n      props: {\n        invoiceNumber: inv.invoiceNumber,\n        amount: inv.amount,\n        status: inv.status as \"draft\" | \"open\" | \"paid\" | \"void\",\n        dueDate: inv.dueDate,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Catalog Info for Display\n// =============================================================================\n\nconst componentCatalogInfo = Object.entries(stripeCatalog.data.components).map(\n  ([name, def]) => ({\n    name,\n    description: def.description,\n  }),\n);\n\nconst actionsCatalogInfo = Object.entries(stripeCatalog.data.actions).map(\n  ([name, def]) => ({\n    name,\n    description: def.description,\n  }),\n);\n\n// =============================================================================\n// Home Component\n// =============================================================================\n\nconst Home = (_props: ExtensionContextValue) => {\n  const [prompt, setPrompt] = useState(\"\");\n  const [isGenerating, setIsGenerating] = useState(false);\n  const [isLoading, setIsLoading] = useState(true);\n  const [currentSpec, setCurrentSpec] = useState<Spec | null>(null);\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [error, setError] = useState<string | null>(null);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  // Load initial data\n  useEffect(() => {\n    async function loadData() {\n      setIsLoading(true);\n      try {\n        await executeAction(\"refreshData\", {}, handleSetState, {});\n      } catch (err) {\n        console.error(\"Failed to load data:\", err);\n        setError(\"Failed to load Stripe data\");\n      } finally {\n        setIsLoading(false);\n      }\n    }\n    loadData();\n  }, [handleSetState]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n\n    setIsGenerating(true);\n    setError(null);\n\n    try {\n      const dataSnapshot = JSON.stringify(data, null, 2);\n\n      await streamSpec(\n        API_GENERATE_URL,\n        {\n          prompt: `${prompt}\\n\\nAVAILABLE STRIPE DATA (use this real data, do NOT invent fake numbers):\\n${dataSnapshot}`,\n          systemPrompt: stripeCatalog.prompt({\n            system:\n              \"You are a Stripe dashboard widget builder. Generate UI specs for displaying Stripe data.\",\n            customRules: [\n              'LAYOUT CONSTRAINT: This UI renders inside a narrow Stripe dashboard drawer (~320px wide). NEVER place more than 2 small items side-by-side horizontally. Prefer vertical (stacked) layouts. Use direction:\"horizontal\" sparingly and only for very compact items like a label+value pair.',\n              'DATA: The user prompt includes real Stripe data under \"AVAILABLE STRIPE DATA\". You MUST use these real values when setting /state. NEVER invent or hallucinate numbers. If the data you need is not available, say so in a Text element rather than making up data. The data keys are: customers (data[], total), payments (data[], total, totalVolume, successRate), subscriptions (data[], total, active, trialing, pastDue, canceled), invoices (data[], total, totalAmount, paid, open, overdue).',\n            ],\n          }),\n        },\n        (spec) => setCurrentSpec(spec),\n      );\n    } catch (err) {\n      // Use dynamic specs with real data as fallback\n      const lowerPrompt = prompt.toLowerCase();\n\n      if (\n        lowerPrompt.includes(\"payment\") ||\n        lowerPrompt.includes(\"transaction\")\n      ) {\n        setCurrentSpec(createPaymentsSpec(data));\n      } else if (\n        lowerPrompt.includes(\"subscription\") ||\n        lowerPrompt.includes(\"recurring\")\n      ) {\n        setCurrentSpec(createSubscriptionsSpec(data));\n      } else if (\n        lowerPrompt.includes(\"customer\") ||\n        lowerPrompt.includes(\"user\")\n      ) {\n        setCurrentSpec(createCustomersSpec(data));\n      } else if (\n        lowerPrompt.includes(\"invoice\") ||\n        lowerPrompt.includes(\"billing\")\n      ) {\n        setCurrentSpec(createInvoicesSpec(data));\n      } else {\n        setCurrentSpec(createRevenueSpec(data));\n      }\n\n      setError(\n        `Using local generation (${err instanceof Error ? err.message : \"API unavailable\"})`,\n      );\n    } finally {\n      setIsGenerating(false);\n    }\n  };\n\n  return (\n    <ContextView\n      title=\"json-render Demo\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      externalLink={{\n        label: \"json-render docs\",\n        href: \"https://github.com/vercel-labs/json-render\",\n      }}\n    >\n      {isLoading ? (\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ font: \"body\", color: \"secondary\" }}>\n            Loading Stripe data...\n          </Box>\n        </Box>\n      ) : (\n        <Tabs fitted>\n          <TabList>\n            <Tab id=\"generate\">Generate</Tab>\n            <Tab id=\"components\">Components</Tab>\n            <Tab id=\"actions\">Actions</Tab>\n          </TabList>\n          <TabPanels>\n            {/* Generate Tab */}\n            <TabPanel id=\"generate\">\n              <Box css={{ stack: \"y\", gap: \"medium\", paddingY: \"medium\" }}>\n                <TextField\n                  label=\"Describe the dashboard you want\"\n                  placeholder=\"e.g., Show MRR, payments, or subscriptions\"\n                  value={prompt}\n                  onChange={(e) => setPrompt(e.target.value)}\n                />\n                <Button\n                  type=\"primary\"\n                  onPress={handleGenerate}\n                  disabled={isGenerating || !prompt.trim()}\n                >\n                  {isGenerating ? \"Generating...\" : \"Generate\"}\n                </Button>\n\n                {error && (\n                  <Box css={{ color: \"secondary\", font: \"caption\" }}>\n                    {error}\n                  </Box>\n                )}\n\n                {currentSpec && (\n                  <>\n                    <Divider />\n                    <StripeRenderer\n                      spec={currentSpec}\n                      data={data}\n                      setData={handleSetState}\n                      loading={isGenerating}\n                    />\n                  </>\n                )}\n\n                {(\n                  data._actionResult as\n                    | { message?: string; url?: string }\n                    | undefined\n                )?.message && (\n                  <Banner\n                    title={(data._actionResult as { message: string }).message}\n                    description={\n                      (data._actionResult as { url?: string }).url\n                        ? \"Open in Stripe Dashboard\"\n                        : undefined\n                    }\n                    actions={\n                      (data._actionResult as { url?: string }).url ? (\n                        <Link\n                          href={(data._actionResult as { url: string }).url}\n                          external\n                        >\n                          Open\n                        </Link>\n                      ) : undefined\n                    }\n                    onDismiss={() =>\n                      setState((prev) => {\n                        const next = { ...prev };\n                        delete next._actionResult;\n                        return next;\n                      })\n                    }\n                  />\n                )}\n\n                <Divider />\n                <Box css={{ font: \"caption\", color: \"secondary\" }}>\n                  Try: &quot;Show revenue metrics&quot; &bull; &quot;Show recent\n                  payments&quot; &bull; &quot;Show subscriptions&quot; &bull;\n                  &quot;Show customers&quot; &bull; &quot;Show invoices&quot;\n                </Box>\n              </Box>\n            </TabPanel>\n\n            {/* Components Catalog Tab */}\n            <TabPanel id=\"components\">\n              <Box css={{ paddingY: \"medium\" }}>\n                <Box css={{ font: \"subheading\", marginBottom: \"medium\" }}>\n                  Available Components ({componentCatalogInfo.length})\n                </Box>\n                <List>\n                  {componentCatalogInfo.map((comp) => (\n                    <ListItem\n                      key={comp.name}\n                      title={comp.name}\n                      secondaryTitle={comp.description}\n                    />\n                  ))}\n                </List>\n              </Box>\n            </TabPanel>\n\n            {/* Actions Catalog Tab */}\n            <TabPanel id=\"actions\">\n              <Box css={{ paddingY: \"medium\" }}>\n                <Box css={{ font: \"subheading\", marginBottom: \"medium\" }}>\n                  Available Actions ({actionsCatalogInfo.length})\n                </Box>\n                <List>\n                  {actionsCatalogInfo.map((action) => (\n                    <ListItem\n                      key={action.name}\n                      title={action.name}\n                      secondaryTitle={action.description}\n                    />\n                  ))}\n                </List>\n              </Box>\n            </TabPanel>\n          </TabPanels>\n        </Tabs>\n      )}\n    </ContextView>\n  );\n};\n\nexport default Home;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Invoices.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createInvoicesSpec(data: Record<string, unknown>): Spec {\n  const invoices = data.invoices as\n    | {\n        data?: Array<{\n          id: string;\n          invoiceNumber: string;\n          amount: number;\n          status: string;\n          dueDate: string;\n          customerEmail: string;\n          formattedAmount: string;\n        }>;\n        total?: number;\n        outstanding?: string;\n        paid?: string;\n        overdue?: string;\n        hasMore?: boolean;\n      }\n    | undefined;\n\n  const invoicesList = invoices?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\n        \"heading\",\n        \"metrics\",\n        \"alert\",\n        \"filters\",\n        \"list\",\n        \"pagination\",\n      ],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Invoices\", size: \"xlarge\" },\n      children: [],\n    },\n    metrics: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"outstandingMetric\", \"paidMetric\", \"overdueMetric\"],\n    },\n    outstandingMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Outstanding\",\n        value: invoices?.outstanding ?? \"$0\",\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    paidMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Paid\",\n        value: invoices?.paid ?? \"$0\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    overdueMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Overdue\",\n        value: invoices?.overdue ?? \"$0\",\n        changeType: invoices?.overdue !== \"$0.00\" ? \"negative\" : \"neutral\",\n      },\n      children: [],\n    },\n    alert: {\n      type: \"Banner\",\n      props: {\n        title:\n          invoices?.overdue !== \"$0.00\"\n            ? `You have ${invoices?.overdue} in overdue invoices`\n            : \"All invoices are up to date\",\n        type: invoices?.overdue !== \"$0.00\" ? \"caution\" : \"default\",\n      },\n      children: [],\n    },\n    filters: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"refreshBtn\", \"createBtn\", \"exportBtn\"],\n    },\n    refreshBtn: {\n      type: \"Button\",\n      props: { label: \"Refresh\", action: \"refreshInvoices\", type: \"secondary\" },\n      children: [],\n    },\n    createBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Create Invoice\",\n        action: \"openDashboard\",\n        actionParams: { page: \"invoices\" },\n        type: \"primary\",\n      },\n      children: [],\n    },\n    exportBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Export CSV\",\n        action: \"exportData\",\n        actionParams: { format: \"csv\", dataType: \"invoices\" },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    list: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: invoicesList.slice(0, 10).map((_, i) => `invoice${i}`),\n    },\n    pagination: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: invoices?.hasMore ? [\"loadMore\"] : [],\n    },\n    loadMore: {\n      type: \"Button\",\n      props: {\n        label: \"Load More\",\n        action: \"fetchInvoices\",\n        actionParams: { limit: 10 },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  invoicesList.slice(0, 10).forEach((inv, i) => {\n    elements[`invoice${i}`] = {\n      type: \"InvoiceCard\",\n      props: {\n        invoiceNumber: inv.invoiceNumber,\n        amount: inv.amount,\n        currency: \"usd\",\n        status: inv.status as\n          | \"draft\"\n          | \"open\"\n          | \"paid\"\n          | \"void\"\n          | \"uncollectible\",\n        dueDate: inv.dueDate,\n        customerEmail: inv.customerEmail,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst Invoices = (_props: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    const loadData = async () => {\n      setLoading(true);\n      await executeAction(\"fetchInvoices\", { limit: 10 }, handleSetState, {});\n      setLoading(false);\n    };\n    loadData();\n  }, [handleSetState]);\n\n  useEffect(() => {\n    if (!loading && Object.keys(data).length > 0) {\n      setSpec(createInvoicesSpec(data));\n    }\n  }, [data, loading]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, systemPrompt }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createInvoicesSpec(data));\n    }\n\n    setGenerating(false);\n  };\n\n  if (loading) {\n    return (\n      <ContextView title=\"Invoices\" brandColor=\"#635BFF\" brandIcon={BrandIcon}>\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading invoices...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Invoices\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\n              \"openDashboard\",\n              { page: \"invoices\" },\n              handleSetState,\n              data,\n            )\n          }\n        >\n          Open in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the invoices view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default Invoices;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/PaymentDetails.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createPaymentDetailSpec(\n  data: Record<string, unknown>,\n  paymentId: string,\n): Spec {\n  const payments = data.payments as\n    | {\n        data?: Array<{\n          id: string;\n          amount: number;\n          currency: string;\n          status: string;\n          description: string;\n          created: string;\n          formattedAmount: string;\n          customerId?: string;\n        }>;\n      }\n    | undefined;\n\n  const payment = payments?.data?.find((p) => p.id === paymentId);\n\n  const refunds = data.paymentRefunds as\n    | {\n        data?: Array<{\n          id: string;\n          amount: number;\n          status: string;\n          reason: string;\n          created: string;\n          formattedAmount: string;\n        }>;\n        total?: number;\n      }\n    | undefined;\n\n  const refundsList = refunds?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"header\", \"details\", \"actions\", \"refundsSection\"],\n    },\n    header: {\n      type: \"Stack\",\n      props: {\n        direction: \"horizontal\",\n        gap: \"medium\",\n        distribute: \"space-between\",\n      },\n      children: [\"paymentInfo\", \"statusBadge\"],\n    },\n    paymentInfo: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"xsmall\" },\n      children: [\"paymentAmount\", \"paymentId\"],\n    },\n    paymentAmount: {\n      type: \"Heading\",\n      props: { text: payment?.formattedAmount ?? \"$0.00\", size: \"xlarge\" },\n      children: [],\n    },\n    paymentId: {\n      type: \"Text\",\n      props: { content: paymentId, color: \"secondary\", size: \"small\" },\n      children: [],\n    },\n    statusBadge: {\n      type: \"Badge\",\n      props: {\n        label: payment?.status ?? \"unknown\",\n        type:\n          payment?.status === \"succeeded\"\n            ? \"positive\"\n            : payment?.status === \"pending\"\n              ? \"warning\"\n              : \"negative\",\n      },\n      children: [],\n    },\n    details: {\n      type: \"PropertyList\",\n      props: { orientation: \"vertical\" },\n      children: [\n        \"detailDescription\",\n        \"detailCreated\",\n        \"detailCurrency\",\n        \"detailCustomer\",\n      ],\n    },\n    detailDescription: {\n      type: \"PropertyListItem\",\n      props: {\n        label: \"Description\",\n        value: payment?.description ?? \"No description\",\n      },\n      children: [],\n    },\n    detailCreated: {\n      type: \"PropertyListItem\",\n      props: { label: \"Created\", value: payment?.created ?? \"Unknown\" },\n      children: [],\n    },\n    detailCurrency: {\n      type: \"PropertyListItem\",\n      props: {\n        label: \"Currency\",\n        value: (payment?.currency ?? \"usd\").toUpperCase(),\n      },\n      children: [],\n    },\n    detailCustomer: {\n      type: \"PropertyListItem\",\n      props: { label: \"Customer\", value: payment?.customerId ?? \"Guest\" },\n      children: [],\n    },\n    actions: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children:\n        payment?.status === \"succeeded\"\n          ? [\"refundBtn\", \"viewBtn\"]\n          : [\"viewBtn\"],\n    },\n    refundBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Refund Payment\",\n        action: \"refundPayment\",\n        actionParams: { paymentId },\n        type: \"destructive\",\n      },\n      children: [],\n    },\n    viewBtn: {\n      type: \"Button\",\n      props: {\n        label: \"View in Dashboard\",\n        action: \"viewPayment\",\n        actionParams: { paymentId },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    refundsSection: {\n      type: \"Accordion\",\n      props: {},\n      children: [\"refundsAccordion\"],\n    },\n    refundsAccordion: {\n      type: \"AccordionItem\",\n      props: {\n        title: `Refunds (${refunds?.total ?? 0})`,\n        defaultOpen: refundsList.length > 0,\n      },\n      children: refundsList.length > 0 ? [\"refundsList\"] : [\"noRefunds\"],\n    },\n    refundsList: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: refundsList.slice(0, 5).map((_, i) => `refund${i}`),\n    },\n    noRefunds: {\n      type: \"Text\",\n      props: { content: \"No refunds\", color: \"secondary\" },\n      children: [],\n    },\n  };\n\n  refundsList.slice(0, 5).forEach((r, i) => {\n    elements[`refund${i}`] = {\n      type: \"RefundCard\",\n      props: {\n        amount: r.amount,\n        currency: \"usd\",\n        status: r.status as \"pending\" | \"succeeded\" | \"failed\" | \"canceled\",\n        reason: r.reason,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst PaymentDetails = ({ environment }: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  const paymentId = environment?.objectContext?.id ?? \"\";\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    const loadData = async () => {\n      if (!paymentId) {\n        setLoading(false);\n        return;\n      }\n\n      setLoading(true);\n\n      await Promise.all([\n        executeAction(\"fetchPayments\", { limit: 100 }, handleSetState, {}),\n        executeAction(\n          \"fetchRefunds\",\n          { paymentIntentId: paymentId, limit: 10 },\n          (updater) => {\n            setState((prev) => {\n              const next = updater(prev);\n              return { ...prev, paymentRefunds: next.refunds };\n            });\n          },\n          {},\n        ),\n      ]);\n\n      setLoading(false);\n    };\n    loadData();\n  }, [paymentId, handleSetState]);\n\n  useEffect(() => {\n    if (!loading && paymentId) {\n      setSpec(createPaymentDetailSpec(data, paymentId));\n    }\n  }, [data, loading, paymentId]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          prompt: `For payment ${paymentId}: ${prompt}`,\n          systemPrompt,\n        }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createPaymentDetailSpec(data, paymentId));\n    }\n\n    setGenerating(false);\n  };\n\n  if (!paymentId) {\n    return (\n      <ContextView\n        title=\"Payment Details\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box css={{ color: \"secondary\", padding: \"large\" }}>\n          No payment selected. Please select a payment from the list.\n        </Box>\n      </ContextView>\n    );\n  }\n\n  if (loading) {\n    return (\n      <ContextView\n        title=\"Payment Details\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading payment details...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Payment Details\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\"viewPayment\", { paymentId }, handleSetState, data)\n          }\n        >\n          View in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the payment view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default PaymentDetails;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Payments.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createPaymentsSpec(data: Record<string, unknown>): Spec {\n  const payments = data.payments as\n    | {\n        data?: Array<{\n          id: string;\n          formattedAmount: string;\n          status: string;\n          description: string;\n          created: string;\n        }>;\n        total?: number;\n        totalVolume?: string;\n        successRate?: string;\n        hasMore?: boolean;\n      }\n    | undefined;\n\n  const paymentsList = payments?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"metrics\", \"filters\", \"list\", \"pagination\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Payments\", size: \"xlarge\" },\n      children: [],\n    },\n    metrics: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"volumeMetric\", \"rateMetric\", \"countMetric\"],\n    },\n    volumeMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Volume\",\n        value: payments?.totalVolume ?? \"$0\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    rateMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Success Rate\",\n        value: payments?.successRate ?? \"0%\",\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    countMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Payments\",\n        value: String(payments?.total ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    filters: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"refreshBtn\", \"exportBtn\"],\n    },\n    refreshBtn: {\n      type: \"Button\",\n      props: { label: \"Refresh\", action: \"refreshPayments\", type: \"secondary\" },\n      children: [],\n    },\n    exportBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Export CSV\",\n        action: \"exportData\",\n        actionParams: { format: \"csv\", dataType: \"payments\" },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    list: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: paymentsList.slice(0, 10).map((_, i) => `payment${i}`),\n    },\n    pagination: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: payments?.hasMore ? [\"loadMore\"] : [],\n    },\n    loadMore: {\n      type: \"Button\",\n      props: {\n        label: \"Load More\",\n        action: \"fetchPayments\",\n        actionParams: {\n          limit: 10,\n          startingAfter: paymentsList[paymentsList.length - 1]?.id,\n        },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  paymentsList.slice(0, 10).forEach((p, i) => {\n    elements[`payment${i}`] = {\n      type: \"PaymentCard\",\n      props: {\n        amount: 0,\n        currency: \"usd\",\n        status: p.status as \"succeeded\" | \"pending\" | \"failed\" | \"canceled\",\n        description: `${p.formattedAmount} - ${p.description}`,\n        paymentId: p.id,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst Payments = (_props: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    const loadData = async () => {\n      setLoading(true);\n      await executeAction(\"fetchPayments\", { limit: 10 }, handleSetState, {});\n      setLoading(false);\n    };\n    loadData();\n  }, [handleSetState]);\n\n  useEffect(() => {\n    if (!loading && Object.keys(data).length > 0) {\n      setSpec(createPaymentsSpec(data));\n    }\n  }, [data, loading]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, systemPrompt }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createPaymentsSpec(data));\n    }\n\n    setGenerating(false);\n  };\n\n  if (loading) {\n    return (\n      <ContextView title=\"Payments\" brandColor=\"#635BFF\" brandIcon={BrandIcon}>\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading payments...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Payments\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\n              \"openDashboard\",\n              { page: \"payments\" },\n              handleSetState,\n              data,\n            )\n          }\n        >\n          Open in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the payments view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default Payments;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Products.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createProductsSpec(data: Record<string, unknown>): Spec {\n  const products = data.products as\n    | {\n        data?: Array<{\n          id: string;\n          name: string;\n          description: string;\n          active: boolean;\n          created: string;\n        }>;\n        total?: number;\n        hasMore?: boolean;\n      }\n    | undefined;\n\n  const prices = data.prices as\n    | {\n        data?: Array<{\n          id: string;\n          productId: string;\n          formattedAmount: string;\n          type: string;\n          recurring?: { interval: string };\n        }>;\n      }\n    | undefined;\n\n  const productsList = products?.data ?? [];\n  const pricesList = prices?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\"heading\", \"metrics\", \"filters\", \"list\", \"pagination\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Products & Prices\", size: \"xlarge\" },\n      children: [],\n    },\n    metrics: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\"productsMetric\", \"pricesMetric\", \"activeMetric\"],\n    },\n    productsMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Products\",\n        value: String(products?.total ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    pricesMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Total Prices\",\n        value: String(pricesList.length ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    activeMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Active Products\",\n        value: String(productsList.filter((p) => p.active).length ?? 0),\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    filters: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"refreshBtn\", \"createBtn\"],\n    },\n    refreshBtn: {\n      type: \"Button\",\n      props: { label: \"Refresh\", action: \"fetchProducts\", type: \"secondary\" },\n      children: [],\n    },\n    createBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Create Product\",\n        action: \"openDashboard\",\n        actionParams: { page: \"products\" },\n        type: \"primary\",\n      },\n      children: [],\n    },\n    list: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: productsList.slice(0, 10).map((_, i) => `product${i}`),\n    },\n    pagination: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: products?.hasMore ? [\"loadMore\"] : [],\n    },\n    loadMore: {\n      type: \"Button\",\n      props: {\n        label: \"Load More\",\n        action: \"fetchProducts\",\n        actionParams: { limit: 10 },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  productsList.slice(0, 10).forEach((p, i) => {\n    const productPrices = pricesList.filter((pr) => pr.productId === p.id);\n    const priceDisplay =\n      productPrices.length > 0\n        ? productPrices\n            .map(\n              (pr) =>\n                `${pr.formattedAmount}${pr.recurring ? `/${pr.recurring.interval}` : \"\"}`,\n            )\n            .join(\", \")\n        : \"No prices\";\n\n    elements[`product${i}`] = {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: [`productCard${i}`],\n    };\n\n    elements[`productCard${i}`] = {\n      type: \"PropertyList\",\n      props: { orientation: \"vertical\" },\n      children: [\n        `productName${i}`,\n        `productDesc${i}`,\n        `productPrice${i}`,\n        `productStatus${i}`,\n      ],\n    };\n\n    elements[`productName${i}`] = {\n      type: \"PropertyListItem\",\n      props: { label: \"Name\", value: p.name },\n      children: [],\n    };\n\n    elements[`productDesc${i}`] = {\n      type: \"PropertyListItem\",\n      props: { label: \"Description\", value: p.description || \"No description\" },\n      children: [],\n    };\n\n    elements[`productPrice${i}`] = {\n      type: \"PropertyListItem\",\n      props: { label: \"Prices\", value: priceDisplay },\n      children: [],\n    };\n\n    elements[`productStatus${i}`] = {\n      type: \"PropertyListItem\",\n      props: { label: \"Status\", value: p.active ? \"Active\" : \"Archived\" },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst Products = (_props: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    const loadData = async () => {\n      setLoading(true);\n      await Promise.all([\n        executeAction(\"fetchProducts\", { limit: 10 }, handleSetState, {}),\n        executeAction(\"fetchPrices\", { limit: 50 }, handleSetState, {}),\n      ]);\n      setLoading(false);\n    };\n    loadData();\n  }, [handleSetState]);\n\n  useEffect(() => {\n    if (!loading && Object.keys(data).length > 0) {\n      setSpec(createProductsSpec(data));\n    }\n  }, [data, loading]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, systemPrompt }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createProductsSpec(data));\n    }\n\n    setGenerating(false);\n  };\n\n  if (loading) {\n    return (\n      <ContextView title=\"Products\" brandColor=\"#635BFF\" brandIcon={BrandIcon}>\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading products...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Products & Prices\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\n              \"openDashboard\",\n              { page: \"products\" },\n              handleSetState,\n              data,\n            )\n          }\n        >\n          Open in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the products view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default Products;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/src/views/Subscriptions.tsx",
    "content": "import { useState, useEffect, useCallback } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  ContextView,\n  Button,\n  TextField,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport BrandIcon from \"./brand_icon.svg\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\n\n// =============================================================================\n// Dynamic Spec Creation\n// =============================================================================\n\nfunction createSubscriptionsSpec(data: Record<string, unknown>): Spec {\n  const subscriptions = data.subscriptions as\n    | {\n        data?: Array<{\n          id: string;\n          planName: string;\n          status: string;\n          amount: number;\n          interval: string;\n          currentPeriodEnd: string;\n          customerId: string;\n        }>;\n        total?: number;\n        active?: number;\n        trialing?: number;\n        pastDue?: number;\n        canceled?: number;\n        hasMore?: boolean;\n      }\n    | undefined;\n\n  const subscriptionsList = subscriptions?.data ?? [];\n\n  const elements: Spec[\"elements\"] = {\n    root: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"large\" },\n      children: [\n        \"heading\",\n        \"metrics\",\n        \"alert\",\n        \"filters\",\n        \"list\",\n        \"pagination\",\n      ],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Subscriptions\", size: \"xlarge\" },\n      children: [],\n    },\n    metrics: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"medium\" },\n      children: [\n        \"activeMetric\",\n        \"trialingMetric\",\n        \"pastDueMetric\",\n        \"canceledMetric\",\n      ],\n    },\n    activeMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Active\",\n        value: String(subscriptions?.active ?? 0),\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    trialingMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Trialing\",\n        value: String(subscriptions?.trialing ?? 0),\n        changeType: \"positive\",\n      },\n      children: [],\n    },\n    pastDueMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Past Due\",\n        value: String(subscriptions?.pastDue ?? 0),\n        changeType: subscriptions?.pastDue ? \"negative\" : \"neutral\",\n      },\n      children: [],\n    },\n    canceledMetric: {\n      type: \"Metric\",\n      props: {\n        label: \"Canceled\",\n        value: String(subscriptions?.canceled ?? 0),\n        changeType: \"neutral\",\n      },\n      children: [],\n    },\n    alert: {\n      type: \"Banner\",\n      props: {\n        title: subscriptions?.pastDue\n          ? `${subscriptions.pastDue} subscriptions need attention`\n          : `${subscriptions?.trialing ?? 0} subscriptions trialing`,\n        type: subscriptions?.pastDue ? \"caution\" : \"default\",\n      },\n      children: [],\n    },\n    filters: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: [\"refreshBtn\", \"exportBtn\"],\n    },\n    refreshBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Refresh\",\n        action: \"refreshSubscriptions\",\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    exportBtn: {\n      type: \"Button\",\n      props: {\n        label: \"Export CSV\",\n        action: \"exportData\",\n        actionParams: { format: \"csv\", dataType: \"subscriptions\" },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n    list: {\n      type: \"Stack\",\n      props: { direction: \"vertical\", gap: \"small\" },\n      children: subscriptionsList\n        .slice(0, 10)\n        .map((_, i) => `subscription${i}`),\n    },\n    pagination: {\n      type: \"Stack\",\n      props: { direction: \"horizontal\", gap: \"small\" },\n      children: subscriptions?.hasMore ? [\"loadMore\"] : [],\n    },\n    loadMore: {\n      type: \"Button\",\n      props: {\n        label: \"Load More\",\n        action: \"fetchSubscriptions\",\n        actionParams: { limit: 10 },\n        type: \"secondary\",\n      },\n      children: [],\n    },\n  };\n\n  subscriptionsList.slice(0, 10).forEach((s, i) => {\n    elements[`subscription${i}`] = {\n      type: \"SubscriptionCard\",\n      props: {\n        planName: s.planName,\n        status: s.status as\n          | \"active\"\n          | \"trialing\"\n          | \"past_due\"\n          | \"canceled\"\n          | \"unpaid\",\n        amount: s.amount,\n        currency: \"usd\",\n        interval: s.interval as \"month\" | \"year\",\n        currentPeriodEnd: s.currentPeriodEnd,\n      },\n      children: [],\n    };\n  });\n\n  return { root: \"root\", elements };\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nconst Subscriptions = (_props: ExtensionContextValue) => {\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [prompt, setPrompt] = useState(\"\");\n  const [generating, setGenerating] = useState(false);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    const loadData = async () => {\n      setLoading(true);\n      await executeAction(\n        \"fetchSubscriptions\",\n        { limit: 10 },\n        handleSetState,\n        {},\n      );\n      setLoading(false);\n    };\n    loadData();\n  }, [handleSetState]);\n\n  useEffect(() => {\n    if (!loading && Object.keys(data).length > 0) {\n      setSpec(createSubscriptionsSpec(data));\n    }\n  }, [data, loading]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setGenerating(true);\n\n    const systemPrompt = stripeCatalog.prompt();\n\n    try {\n      const response = await fetch(API_GENERATE_URL, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, systemPrompt }),\n      });\n\n      const result = await response.json();\n      if (result.spec) {\n        setSpec(result.spec);\n      }\n    } catch (error) {\n      console.error(\"Generation error:\", error);\n      setSpec(createSubscriptionsSpec(data));\n    }\n\n    setGenerating(false);\n  };\n\n  if (loading) {\n    return (\n      <ContextView\n        title=\"Subscriptions\"\n        brandColor=\"#635BFF\"\n        brandIcon={BrandIcon}\n      >\n        <Box\n          css={{\n            stack: \"y\",\n            gap: \"medium\",\n            alignX: \"center\",\n            paddingY: \"xlarge\",\n          }}\n        >\n          <Spinner size=\"large\" />\n          <Box css={{ color: \"secondary\" }}>Loading subscriptions...</Box>\n        </Box>\n      </ContextView>\n    );\n  }\n\n  return (\n    <ContextView\n      title=\"Subscriptions\"\n      brandColor=\"#635BFF\"\n      brandIcon={BrandIcon}\n      actions={\n        <Button\n          type=\"primary\"\n          onPress={() =>\n            executeAction(\n              \"openDashboard\",\n              { page: \"subscriptions\" },\n              handleSetState,\n              data,\n            )\n          }\n        >\n          Open in Dashboard\n        </Button>\n      }\n    >\n      <Box css={{ stack: \"y\", gap: \"medium\" }}>\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <Box css={{ width: \"fill\" }}>\n            <TextField\n              label=\"\"\n              placeholder=\"Describe the subscriptions view you want...\"\n              value={prompt}\n              onChange={(e) => setPrompt(e.target.value)}\n            />\n          </Box>\n          <Box css={{ alignSelfY: \"center\" }}>\n            <Button\n              type=\"primary\"\n              onPress={handleGenerate}\n              disabled={generating || !prompt.trim()}\n            >\n              {generating ? \"Generating...\" : \"Generate\"}\n            </Button>\n          </Box>\n        </Box>\n\n        {spec && (\n          <StripeRenderer\n            spec={spec}\n            data={data}\n            setData={handleSetState}\n            loading={generating}\n          />\n        )}\n      </Box>\n    </ContextView>\n  );\n};\n\nexport default Subscriptions;\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/stripe-app.template.json",
    "content": "{\n    \"id\": \"com.example.json-render-demo\",\n    \"version\": \"0.0.1\",\n    \"name\": \"json-render Demo\",\n    \"icon\": \"\",\n    \"permissions\": [\n        {\n            \"permission\": \"customer_read\",\n            \"purpose\": \"Display customer information in dashboards\"\n        },\n        {\n            \"permission\": \"customer_write\",\n            \"purpose\": \"Create and manage customers\"\n        },\n        {\n            \"permission\": \"payment_intent_read\",\n            \"purpose\": \"Display payment information in dashboards\"\n        },\n        {\n            \"permission\": \"charge_read\",\n            \"purpose\": \"Display charge information and details\"\n        },\n        {\n            \"permission\": \"subscription_read\",\n            \"purpose\": \"Display subscription information in dashboards\"\n        },\n        {\n            \"permission\": \"subscription_write\",\n            \"purpose\": \"Manage subscriptions\"\n        },\n        {\n            \"permission\": \"invoice_read\",\n            \"purpose\": \"Display invoice information in dashboards\"\n        },\n        {\n            \"permission\": \"invoice_write\",\n            \"purpose\": \"Send and manage invoices\"\n        },\n        {\n            \"permission\": \"charge_write\",\n            \"purpose\": \"Process charges and refunds\"\n        },\n        {\n            \"permission\": \"product_read\",\n            \"purpose\": \"Display product catalog\"\n        },\n        {\n            \"permission\": \"product_write\",\n            \"purpose\": \"Create and manage products\"\n        },\n        {\n            \"permission\": \"plan_read\",\n            \"purpose\": \"Display pricing information\"\n        },\n        {\n            \"permission\": \"plan_write\",\n            \"purpose\": \"Create and manage prices\"\n        },\n        {\n            \"permission\": \"balance_read\",\n            \"purpose\": \"Display account balance\"\n        },\n        {\n            \"permission\": \"payout_read\",\n            \"purpose\": \"Display payout information\"\n        },\n        {\n            \"permission\": \"event_read\",\n            \"purpose\": \"Display webhook events\"\n        },\n        {\n            \"permission\": \"dispute_read\",\n            \"purpose\": \"Display dispute information\"\n        },\n        {\n            \"permission\": \"coupon_read\",\n            \"purpose\": \"Display coupon information\"\n        },\n        {\n            \"permission\": \"coupon_write\",\n            \"purpose\": \"Create and manage coupons\"\n        }\n    ],\n    \"ui_extension\": {\n        \"views\": [\n            {\n                \"viewport\": \"stripe.dashboard.home.overview\",\n                \"component\": \"Home\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.customer.list\",\n                \"component\": \"Customers\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.customer.detail\",\n                \"component\": \"CustomerDetails\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.payment.list\",\n                \"component\": \"Payments\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.payment.detail\",\n                \"component\": \"PaymentDetails\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.subscription.list\",\n                \"component\": \"Subscriptions\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.invoice.list\",\n                \"component\": \"Invoices\"\n            },\n            {\n                \"viewport\": \"stripe.dashboard.product.list\",\n                \"component\": \"Products\"\n            }\n        ],\n        \"content_security_policy\": {\n            \"connect-src\": null,\n            \"image-src\": null,\n            \"purpose\": \"No external connections needed for core functionality\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/tsconfig.json",
    "content": "{\n  \"extends\": \"@stripe/ui-extension-tools/tsconfig.ui-extension\"\n}\n"
  },
  {
    "path": "examples/stripe-app/drawer-app/ui-extensions.d.ts",
    "content": "/// <reference types=\"@stripe/ui-extension-tools\" />\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/.env.example",
    "content": "# Stripe App ID (optional - overrides the template default)\n# Get this from your Stripe Apps dashboard after uploading\n# STRIPE_APP_ID=com.example.your-app-id\n\n# Stripe App Name (optional - overrides the template default)\n# STRIPE_APP_NAME=My App Name\n\n# AI Gateway API Key\n# Get your key from Vercel AI Gateway dashboard\nAI_GATEWAY_API_KEY=\n\n# Optional: Stripe API keys if you want to use real data\n# STRIPE_SECRET_KEY=sk_test_...\n# STRIPE_PUBLISHABLE_KEY=pk_test_...\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/CHANGELOG.md",
    "content": "# com.example.json-render-fullpage-demo\n\n## 0.0.10\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n\n## 0.0.9\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.0.8\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n\n## 0.0.7\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react@0.12.0\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/README.md",
    "content": "# Full Page Stripe App (Alpha)\n\nA full-page [Stripe App](https://stripe.com/docs/stripe-apps) example using json-render. This uses the `FullPageView` component and the `stripe.dashboard.fullpage` viewport, which are part of Stripe's **private developer preview**.\n\n## Prerequisites\n\n- [Stripe CLI](https://stripe.com/docs/stripe-cli) with the apps plugin\n- A Stripe account with **full-page apps alpha access** enabled by Stripe\n- Both your `app_id` and `account_id` must be flagged in by Stripe\n\n## Setup\n\n```bash\npnpm install\n\ncp .env.example .env\n# Set STRIPE_APP_ID to your app ID\n\npnpm setup\n```\n\n## Running\n\nStart the API server first (from `../api`):\n\n```bash\ncd ../api && pnpm dev\n```\n\nThen start the Stripe app:\n\n```bash\nstripe apps start\n```\n\nNavigate directly to your full-page app URL (it will not appear in the left navigation):\n\n```\nhttps://dashboard.stripe.com/test/app/<your-app-id>\n```\n\n## Troubleshooting\n\nIf you are redirected to the Dashboard home page, the feature flag is not enabled for your account. Contact your Stripe partner to have both your `app_id` and `account_id` flagged in.\n\n## Resources\n\n- [FullPageView Component](https://docs.stripe.com/stripe-apps/components/fullpageview?app-sdk-version=9)\n- [FullPageTabs Component](https://docs.stripe.com/stripe-apps/components/fullpagetabs?app-sdk-version=9)\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/eslint.config.js",
    "content": "import { config as reactConfig } from \"@internal/eslint-config/react-internal\";\n\n/** @type {import(\"eslint\").Linter.Config[]} */\nexport default [\n  ...reactConfig,\n  {\n    rules: {\n      // Disable prop-types - we use TypeScript for type checking\n      \"react/prop-types\": \"off\",\n      // Allow underscore-prefixed unused variables\n      \"@typescript-eslint/no-unused-vars\": [\n        \"warn\",\n        { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/jest.config.js",
    "content": "/* eslint-env node */\n/* eslint-disable @typescript-eslint/no-var-requires */\nconst UIExtensionsConfig = require(\"@stripe/ui-extension-tools/jest.config.ui-extension\");\n\nmodule.exports = {\n  ...UIExtensionsConfig,\n};\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/package.json",
    "content": "{\n  \"name\": \"com.example.json-render-fullpage-demo\",\n  \"version\": \"0.0.10\",\n  \"description\": \"Full-page Stripe App example (alpha)\",\n  \"private\": true,\n  \"license\": \"~~proprietary~~\",\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@stripe/ui-extension-sdk\": \"9.2.0-alpha.0\",\n    \"stripe\": \"^13.11.0\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"engines\": {\n    \"node\": \">=14\"\n  },\n  \"scripts\": {\n    \"setup\": \"node scripts/setup.mjs\",\n    \"lint\": \"eslint src --max-warnings 0\",\n    \"test\": \"jest\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@stripe/ui-extension-tools\": \"^0.0.1\",\n    \"eslint\": \"^9.39.0\"\n  }\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/scripts/setup.mjs",
    "content": "import { readFileSync, writeFileSync, existsSync } from \"fs\";\nimport { resolve, dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst root = resolve(__dirname, \"..\");\n\nfunction loadEnv() {\n  const envPath = resolve(root, \".env\");\n  if (!existsSync(envPath)) return {};\n  const env = {};\n  for (const line of readFileSync(envPath, \"utf-8\").split(\"\\n\")) {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"#\")) continue;\n    const idx = trimmed.indexOf(\"=\");\n    if (idx === -1) continue;\n    const key = trimmed.slice(0, idx).trim();\n    const val = trimmed.slice(idx + 1).trim();\n    env[key] = val;\n  }\n  return env;\n}\n\nconst env = loadEnv();\nconst templatePath = resolve(root, \"stripe-app.template.json\");\nconst outputPath = resolve(root, \"stripe-app.json\");\n\nconst template = JSON.parse(readFileSync(templatePath, \"utf-8\"));\n\nif (env.STRIPE_APP_ID) {\n  template.id = env.STRIPE_APP_ID;\n  console.log(`Using app ID from .env: ${env.STRIPE_APP_ID}`);\n} else {\n  console.log(`No STRIPE_APP_ID in .env, using template default: ${template.id}`);\n}\n\nif (env.STRIPE_APP_NAME) {\n  template.name = env.STRIPE_APP_NAME;\n}\n\nwriteFileSync(outputPath, JSON.stringify(template, null, 4) + \"\\n\");\nconsole.log(\"Generated stripe-app.json\");\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/config.ts",
    "content": "export const API_GENERATE_URL =\n  \"http://stripe-api-demo.json-render.localhost:1355/api/generate\";\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/render/catalog/actions.ts",
    "content": "import { stripe, formatAmount, formatDate } from \"../../stripe\";\n\n/**\n * Type for setState function\n */\ntype SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n/**\n * Action handlers for Stripe operations\n *\n * These use the real Stripe API via the UI Extension SDK.\n */\nexport const actionHandlers: Record<\n  string,\n  (\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    data: Record<string, unknown>,\n  ) => Promise<void>\n> = {\n  // ===========================================================================\n  // Customer Actions\n  // ===========================================================================\n  fetchCustomers: async (params, setState) => {\n    try {\n      const customers = await stripe.customers.list({\n        limit: (params?.limit as number) ?? 10,\n        email: (params?.email as string) || undefined,\n        starting_after: (params?.startingAfter as string) || undefined,\n      });\n\n      const data = customers.data.map((c) => ({\n        id: c.id,\n        name: c.name ?? c.email ?? \"Unknown\",\n        email: c.email ?? \"\",\n        phone: c.phone ?? \"\",\n        status: c.delinquent ? \"inactive\" : \"active\",\n        created: formatDate(c.created),\n        balance: c.balance,\n        currency: c.currency ?? \"usd\",\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        customers: {\n          data,\n          total: customers.data.length,\n          hasMore: customers.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchCustomers error:\", error);\n    }\n  },\n\n  viewCustomer: async (params) => {\n    if (params?.customerId) {\n      window.open(\n        `https://dashboard.stripe.com/customers/${params.customerId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createCustomer: async (params, setState) => {\n    try {\n      await stripe.customers.create({\n        email: (params?.email as string) ?? \"\",\n        name: (params?.name as string) ?? undefined,\n        phone: (params?.phone as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"createCustomer error:\", error);\n    }\n  },\n\n  updateCustomer: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.customers.update(params.customerId as string, {\n        email: (params?.email as string) ?? undefined,\n        name: (params?.name as string) ?? undefined,\n        phone: (params?.phone as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"updateCustomer error:\", error);\n    }\n  },\n\n  deleteCustomer: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.customers.del(params.customerId as string);\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"deleteCustomer error:\", error);\n    }\n  },\n\n  searchCustomers: async (params, setState) => {\n    try {\n      const customers = await stripe.customers.search({\n        query: (params?.query as string) ?? \"\",\n        limit: (params?.limit as number) ?? 10,\n      });\n\n      const data = customers.data.map((c) => ({\n        id: c.id,\n        name: c.name ?? c.email ?? \"Unknown\",\n        email: c.email ?? \"\",\n        status: c.delinquent ? \"inactive\" : \"active\",\n        created: formatDate(c.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        searchResults: { customers: data, total: customers.data.length },\n      }));\n    } catch (error) {\n      console.error(\"searchCustomers error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Payment Intent Actions\n  // ===========================================================================\n  fetchPayments: async (params, setState) => {\n    try {\n      const payments = await stripe.paymentIntents.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        starting_after: (params?.startingAfter as string) || undefined,\n      });\n\n      const data = payments.data.map((p) => ({\n        id: p.id,\n        amount: p.amount,\n        currency: p.currency,\n        status: p.status,\n        description: p.description ?? `Payment ${p.id.slice(-8)}`,\n        created: formatDate(p.created),\n        formattedAmount: formatAmount(p.amount, p.currency),\n        customerId: p.customer as string,\n      }));\n\n      const succeeded = payments.data.filter((p) => p.status === \"succeeded\");\n      const totalVolume = succeeded.reduce((sum, p) => sum + p.amount, 0);\n      const successRate =\n        payments.data.length > 0\n          ? ((succeeded.length / payments.data.length) * 100).toFixed(1)\n          : \"0\";\n\n      setState((prev) => ({\n        ...prev,\n        payments: {\n          data,\n          total: payments.data.length,\n          totalVolume: formatAmount(totalVolume, \"usd\"),\n          successRate: `${successRate}%`,\n          hasMore: payments.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchPayments error:\", error);\n    }\n  },\n\n  viewPayment: async (params) => {\n    if (params?.paymentId) {\n      window.open(\n        `https://dashboard.stripe.com/payments/${params.paymentId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createPaymentIntent: async (params, setState) => {\n    try {\n      await stripe.paymentIntents.create({\n        amount: (params?.amount as number) ?? 0,\n        currency: (params?.currency as string) ?? \"usd\",\n        customer: (params?.customerId as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"createPaymentIntent error:\", error);\n    }\n  },\n\n  capturePayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.paymentIntents.capture(params.paymentId as string, {\n        amount_to_capture: (params?.amountToCapture as number) ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"capturePayment error:\", error);\n    }\n  },\n\n  cancelPayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.paymentIntents.cancel(params.paymentId as string, {\n        cancellation_reason:\n          (params?.reason as\n            | \"duplicate\"\n            | \"fraudulent\"\n            | \"requested_by_customer\"\n            | \"abandoned\") ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelPayment error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Refund Actions\n  // ===========================================================================\n  fetchRefunds: async (params, setState) => {\n    try {\n      const refunds = await stripe.refunds.list({\n        limit: (params?.limit as number) ?? 10,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n        charge: (params?.chargeId as string) || undefined,\n      });\n\n      const data = refunds.data.map((r) => ({\n        id: r.id,\n        amount: r.amount,\n        currency: r.currency,\n        status: r.status,\n        reason: r.reason,\n        created: formatDate(r.created),\n        formattedAmount: formatAmount(r.amount, r.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        refunds: { data, total: refunds.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchRefunds error:\", error);\n    }\n  },\n\n  refundPayment: async (params, setState) => {\n    try {\n      if (!params?.paymentId) return;\n      await stripe.refunds.create({\n        payment_intent: params.paymentId as string,\n        amount: (params?.amount as number) ?? undefined,\n        reason:\n          (params?.reason as\n            | \"duplicate\"\n            | \"fraudulent\"\n            | \"requested_by_customer\") ?? undefined,\n      });\n      await actionHandlers.fetchPayments({}, setState, {});\n      await actionHandlers.fetchRefunds({}, setState, {});\n    } catch (error) {\n      console.error(\"refundPayment error:\", error);\n    }\n  },\n\n  updateRefund: async (params, setState) => {\n    try {\n      if (!params?.refundId) return;\n      await stripe.refunds.update(params.refundId as string, {\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchRefunds({}, setState, {});\n    } catch (error) {\n      console.error(\"updateRefund error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Charge Actions\n  // ===========================================================================\n  fetchCharges: async (params, setState) => {\n    try {\n      const charges = await stripe.charges.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n      });\n\n      const data = charges.data.map((c) => ({\n        id: c.id,\n        amount: c.amount,\n        currency: c.currency,\n        status: c.status,\n        captured: c.captured,\n        description: c.description,\n        created: formatDate(c.created),\n        formattedAmount: formatAmount(c.amount, c.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        charges: { data, total: charges.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCharges error:\", error);\n    }\n  },\n\n  captureCharge: async (params, setState) => {\n    try {\n      if (!params?.chargeId) return;\n      await stripe.charges.capture(params.chargeId as string, {\n        amount: (params?.amount as number) ?? undefined,\n      });\n      await actionHandlers.fetchCharges({}, setState, {});\n    } catch (error) {\n      console.error(\"captureCharge error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Subscription Actions\n  // ===========================================================================\n  fetchSubscriptions: async (params, setState) => {\n    try {\n      const subscriptions = await stripe.subscriptions.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n        customer: (params?.customerId as string) || undefined,\n        price: (params?.priceId as string) || undefined,\n      });\n\n      const data = subscriptions.data.map((s) => ({\n        id: s.id,\n        planName:\n          s.items.data[0]?.price?.nickname ??\n          s.items.data[0]?.price?.id ??\n          \"Plan\",\n        status: s.status,\n        amount: s.items.data[0]?.price?.unit_amount ?? 0,\n        currency: s.items.data[0]?.price?.currency ?? \"usd\",\n        interval: s.items.data[0]?.price?.recurring?.interval ?? \"month\",\n        customerId: s.customer as string,\n        currentPeriodEnd: formatDate(s.current_period_end),\n        cancelAtPeriodEnd: s.cancel_at_period_end,\n      }));\n\n      const active = subscriptions.data.filter(\n        (s) => s.status === \"active\",\n      ).length;\n      const trialing = subscriptions.data.filter(\n        (s) => s.status === \"trialing\",\n      ).length;\n      const pastDue = subscriptions.data.filter(\n        (s) => s.status === \"past_due\",\n      ).length;\n      const canceled = subscriptions.data.filter(\n        (s) => s.status === \"canceled\",\n      ).length;\n\n      setState((prev) => ({\n        ...prev,\n        subscriptions: {\n          data,\n          total: subscriptions.data.length,\n          active,\n          trialing,\n          pastDue,\n          canceled,\n          hasMore: subscriptions.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchSubscriptions error:\", error);\n    }\n  },\n\n  viewSubscription: async (params) => {\n    if (params?.subscriptionId) {\n      window.open(\n        `https://dashboard.stripe.com/subscriptions/${params.subscriptionId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createSubscription: async (params, setState) => {\n    try {\n      if (!params?.customerId || !params?.priceId) return;\n      await stripe.subscriptions.create({\n        customer: params.customerId as string,\n        items: [\n          {\n            price: params.priceId as string,\n            quantity: (params?.quantity as number) ?? 1,\n          },\n        ],\n        trial_period_days: (params?.trialPeriodDays as number) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"createSubscription error:\", error);\n    }\n  },\n\n  updateSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      const updateParams: Record<string, unknown> = {};\n      if (params.priceId) {\n        updateParams.items = [\n          {\n            price: params.priceId as string,\n            quantity: (params?.quantity as number) ?? 1,\n          },\n        ];\n      }\n      if (params.metadata) {\n        updateParams.metadata = params.metadata;\n      }\n      await stripe.subscriptions.update(\n        params.subscriptionId as string,\n        updateParams,\n      );\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"updateSubscription error:\", error);\n    }\n  },\n\n  cancelSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      if (params.immediately) {\n        await stripe.subscriptions.cancel(params.subscriptionId as string);\n      } else {\n        await stripe.subscriptions.update(params.subscriptionId as string, {\n          cancel_at_period_end: true,\n        });\n      }\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelSubscription error:\", error);\n    }\n  },\n\n  pauseSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      await stripe.subscriptions.update(params.subscriptionId as string, {\n        pause_collection: {\n          behavior: \"mark_uncollectible\",\n          resumes_at: (params?.resumeAt as number) ?? undefined,\n        },\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"pauseSubscription error:\", error);\n    }\n  },\n\n  resumeSubscription: async (params, setState) => {\n    try {\n      if (!params?.subscriptionId) return;\n      await stripe.subscriptions.update(params.subscriptionId as string, {\n        pause_collection: \"\",\n      });\n      await actionHandlers.fetchSubscriptions({}, setState, {});\n    } catch (error) {\n      console.error(\"resumeSubscription error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Invoice Actions\n  // ===========================================================================\n  fetchInvoices: async (params, setState) => {\n    try {\n      const invoices = await stripe.invoices.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n        customer: (params?.customerId as string) || undefined,\n        subscription: (params?.subscriptionId as string) || undefined,\n      });\n\n      const data = invoices.data.map((inv) => ({\n        id: inv.id,\n        invoiceNumber: inv.number ?? inv.id.slice(-8),\n        amount: inv.amount_due,\n        currency: inv.currency,\n        status: inv.status ?? \"draft\",\n        dueDate: inv.due_date ? formatDate(inv.due_date) : null,\n        customerEmail: inv.customer_email ?? \"\",\n        formattedAmount: formatAmount(inv.amount_due, inv.currency),\n        hostedInvoiceUrl: inv.hosted_invoice_url,\n        pdfUrl: inv.invoice_pdf,\n      }));\n\n      const outstanding = invoices.data\n        .filter((i) => i.status === \"open\")\n        .reduce((sum, i) => sum + i.amount_due, 0);\n      const paid = invoices.data\n        .filter((i) => i.status === \"paid\")\n        .reduce((sum, i) => sum + i.amount_paid, 0);\n      const overdue = invoices.data\n        .filter(\n          (i) =>\n            i.status === \"open\" && i.due_date && i.due_date < Date.now() / 1000,\n        )\n        .reduce((sum, i) => sum + i.amount_due, 0);\n\n      setState((prev) => ({\n        ...prev,\n        invoices: {\n          data,\n          total: invoices.data.length,\n          outstanding: formatAmount(outstanding, \"usd\"),\n          paid: formatAmount(paid, \"usd\"),\n          overdue: formatAmount(overdue, \"usd\"),\n          hasMore: invoices.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchInvoices error:\", error);\n    }\n  },\n\n  viewInvoice: async (params) => {\n    if (params?.invoiceId) {\n      window.open(\n        `https://dashboard.stripe.com/invoices/${params.invoiceId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createInvoice: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      await stripe.invoices.create({\n        customer: params.customerId as string,\n        description: (params?.description as string) ?? undefined,\n        days_until_due: (params?.daysUntilDue as number) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"createInvoice error:\", error);\n    }\n  },\n\n  addInvoiceItem: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoiceItems.create({\n        invoice: params.invoiceId as string,\n        amount: (params?.amount as number) ?? 0,\n        currency: (params?.currency as string) ?? \"usd\",\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"addInvoiceItem error:\", error);\n    }\n  },\n\n  finalizeInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.finalizeInvoice(params.invoiceId as string, {\n        auto_advance: (params?.autoAdvance as boolean) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"finalizeInvoice error:\", error);\n    }\n  },\n\n  sendInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.sendInvoice(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"sendInvoice error:\", error);\n    }\n  },\n\n  payInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.pay(params.invoiceId as string, {\n        payment_method: (params?.paymentMethodId as string) ?? undefined,\n      });\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"payInvoice error:\", error);\n    }\n  },\n\n  voidInvoice: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.voidInvoice(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"voidInvoice error:\", error);\n    }\n  },\n\n  markInvoiceUncollectible: async (params, setState) => {\n    try {\n      if (!params?.invoiceId) return;\n      await stripe.invoices.markUncollectible(params.invoiceId as string);\n      await actionHandlers.fetchInvoices({}, setState, {});\n    } catch (error) {\n      console.error(\"markInvoiceUncollectible error:\", error);\n    }\n  },\n\n  downloadInvoicePdf: async (params, _, data) => {\n    try {\n      if (!params?.invoiceId) return;\n      const invoices =\n        (data.invoices as { data: Array<{ id: string; pdfUrl: string }> })\n          ?.data ?? [];\n      const invoice = invoices.find((i) => i.id === params.invoiceId);\n      if (invoice?.pdfUrl) {\n        window.open(invoice.pdfUrl, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"downloadInvoicePdf error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Product & Price Actions\n  // ===========================================================================\n  fetchProducts: async (params, setState) => {\n    try {\n      const products = await stripe.products.list({\n        limit: (params?.limit as number) ?? 10,\n        active: (params?.active as boolean) ?? undefined,\n      });\n\n      const data = products.data.map((p) => ({\n        id: p.id,\n        name: p.name,\n        description: p.description,\n        active: p.active,\n        created: formatDate(p.created),\n        images: p.images,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        products: {\n          data,\n          total: products.data.length,\n          hasMore: products.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchProducts error:\", error);\n    }\n  },\n\n  viewProduct: async (params) => {\n    if (params?.productId) {\n      window.open(\n        `https://dashboard.stripe.com/products/${params.productId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  createProduct: async (params, setState) => {\n    try {\n      if (!params?.name) return;\n      await stripe.products.create({\n        name: params.name as string,\n        description: (params?.description as string) ?? undefined,\n        active: (params?.active as boolean) ?? true,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"createProduct error:\", error);\n    }\n  },\n\n  updateProduct: async (params, setState) => {\n    try {\n      if (!params?.productId) return;\n      await stripe.products.update(params.productId as string, {\n        name: (params?.name as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n        active: (params?.active as boolean) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"updateProduct error:\", error);\n    }\n  },\n\n  archiveProduct: async (params, setState) => {\n    try {\n      if (!params?.productId) return;\n      await stripe.products.update(params.productId as string, {\n        active: false,\n      });\n      await actionHandlers.fetchProducts({}, setState, {});\n    } catch (error) {\n      console.error(\"archiveProduct error:\", error);\n    }\n  },\n\n  fetchPrices: async (params, setState) => {\n    try {\n      const prices = await stripe.prices.list({\n        limit: (params?.limit as number) ?? 10,\n        product: (params?.productId as string) || undefined,\n        active: (params?.active as boolean) ?? undefined,\n        type: (params?.type as \"one_time\" | \"recurring\") || undefined,\n      });\n\n      const data = prices.data.map((p) => ({\n        id: p.id,\n        nickname: p.nickname,\n        unitAmount: p.unit_amount,\n        currency: p.currency,\n        active: p.active,\n        type: p.type,\n        recurring: p.recurring,\n        formattedAmount: formatAmount(p.unit_amount ?? 0, p.currency),\n        productId: typeof p.product === \"string\" ? p.product : p.product?.id,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        prices: { data, total: prices.data.length, hasMore: prices.has_more },\n      }));\n    } catch (error) {\n      console.error(\"fetchPrices error:\", error);\n    }\n  },\n\n  createPrice: async (params, setState) => {\n    try {\n      if (!params?.productId || !params?.unitAmount) return;\n      const recurring = params?.recurring as\n        | { interval: string; intervalCount?: number }\n        | undefined;\n      await stripe.prices.create({\n        product: params.productId as string,\n        unit_amount: params.unitAmount as number,\n        currency: (params?.currency as string) ?? \"usd\",\n        recurring: recurring\n          ? {\n              interval: recurring.interval as \"day\" | \"week\" | \"month\" | \"year\",\n              interval_count: recurring.intervalCount ?? undefined,\n            }\n          : undefined,\n        nickname: (params?.nickname as string) ?? undefined,\n      });\n      await actionHandlers.fetchPrices({}, setState, {});\n    } catch (error) {\n      console.error(\"createPrice error:\", error);\n    }\n  },\n\n  updatePrice: async (params, setState) => {\n    try {\n      if (!params?.priceId) return;\n      await stripe.prices.update(params.priceId as string, {\n        active: (params?.active as boolean) ?? undefined,\n        nickname: (params?.nickname as string) ?? undefined,\n        metadata: (params?.metadata as Record<string, string>) ?? undefined,\n      });\n      await actionHandlers.fetchPrices({}, setState, {});\n    } catch (error) {\n      console.error(\"updatePrice error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Balance & Payout Actions\n  // ===========================================================================\n  fetchBalance: async (_, setState) => {\n    try {\n      const balance = await stripe.balance.retrieve();\n\n      const available = balance.available.reduce((sum, b) => sum + b.amount, 0);\n      const pending = balance.pending.reduce((sum, b) => sum + b.amount, 0);\n\n      setState((prev) => ({\n        ...prev,\n        balance: {\n          available,\n          pending,\n          formattedAvailable: formatAmount(available, \"usd\"),\n          formattedPending: formatAmount(pending, \"usd\"),\n          currency: balance.available[0]?.currency ?? \"usd\",\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchBalance error:\", error);\n    }\n  },\n\n  fetchPayouts: async (params, setState) => {\n    try {\n      const payouts = await stripe.payouts.list({\n        limit: (params?.limit as number) ?? 10,\n        status: (params?.status as string) || undefined,\n      });\n\n      const data = payouts.data.map((p) => ({\n        id: p.id,\n        amount: p.amount,\n        currency: p.currency,\n        status: p.status,\n        arrivalDate: formatDate(p.arrival_date),\n        created: formatDate(p.created),\n        formattedAmount: formatAmount(p.amount, p.currency),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        payouts: {\n          data,\n          total: payouts.data.length,\n          hasMore: payouts.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchPayouts error:\", error);\n    }\n  },\n\n  createPayout: async (params, setState) => {\n    try {\n      if (!params?.amount) return;\n      await stripe.payouts.create({\n        amount: params.amount as number,\n        currency: (params?.currency as string) ?? \"usd\",\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchPayouts({}, setState, {});\n      await actionHandlers.fetchBalance({}, setState, {});\n    } catch (error) {\n      console.error(\"createPayout error:\", error);\n    }\n  },\n\n  cancelPayout: async (params, setState) => {\n    try {\n      if (!params?.payoutId) return;\n      await stripe.payouts.cancel(params.payoutId as string);\n      await actionHandlers.fetchPayouts({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelPayout error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Dispute Actions\n  // ===========================================================================\n  fetchDisputes: async (params, setState) => {\n    try {\n      const disputes = await stripe.disputes.list({\n        limit: (params?.limit as number) ?? 10,\n        charge: (params?.chargeId as string) || undefined,\n      });\n\n      const data = disputes.data.map((d) => ({\n        id: d.id,\n        amount: d.amount,\n        currency: d.currency,\n        status: d.status,\n        reason: d.reason,\n        created: formatDate(d.created),\n        formattedAmount: formatAmount(d.amount, d.currency),\n        evidenceDueBy: d.evidence_details?.due_by\n          ? formatDate(d.evidence_details.due_by)\n          : null,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        disputes: {\n          data,\n          total: disputes.data.length,\n          hasMore: disputes.has_more,\n        },\n      }));\n    } catch (error) {\n      console.error(\"fetchDisputes error:\", error);\n    }\n  },\n\n  viewDispute: async (params) => {\n    if (params?.disputeId) {\n      window.open(\n        `https://dashboard.stripe.com/disputes/${params.disputeId}`,\n        \"_blank\",\n      );\n    }\n  },\n\n  updateDispute: async (params, setState) => {\n    try {\n      if (!params?.disputeId) return;\n      const evidence = params?.evidence as Record<string, string> | undefined;\n      await stripe.disputes.update(params.disputeId as string, {\n        evidence: evidence\n          ? {\n              customer_name: evidence.customerName ?? undefined,\n              customer_email_address:\n                evidence.customerEmailAddress ?? undefined,\n              product_description: evidence.productDescription ?? undefined,\n              uncategorized_text: evidence.uncategorizedText ?? undefined,\n            }\n          : undefined,\n        submit: (params?.submit as boolean) ?? undefined,\n      });\n      await actionHandlers.fetchDisputes({}, setState, {});\n    } catch (error) {\n      console.error(\"updateDispute error:\", error);\n    }\n  },\n\n  closeDispute: async (params, setState) => {\n    try {\n      if (!params?.disputeId) return;\n      await stripe.disputes.close(params.disputeId as string);\n      await actionHandlers.fetchDisputes({}, setState, {});\n    } catch (error) {\n      console.error(\"closeDispute error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Payment Method Actions\n  // ===========================================================================\n  fetchPaymentMethods: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      const paymentMethods = await stripe.paymentMethods.list({\n        customer: params.customerId as string,\n        type: (params?.type as \"card\" | \"us_bank_account\") || \"card\",\n      });\n\n      const data = paymentMethods.data.map((pm) => ({\n        id: pm.id,\n        type: pm.type,\n        card: pm.card\n          ? {\n              brand: pm.card.brand,\n              last4: pm.card.last4,\n              expMonth: pm.card.exp_month,\n              expYear: pm.card.exp_year,\n            }\n          : null,\n        created: formatDate(pm.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        paymentMethods: { data, total: paymentMethods.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchPaymentMethods error:\", error);\n    }\n  },\n\n  attachPaymentMethod: async (params, setState) => {\n    try {\n      if (!params?.paymentMethodId || !params?.customerId) return;\n      await stripe.paymentMethods.attach(params.paymentMethodId as string, {\n        customer: params.customerId as string,\n      });\n      await actionHandlers.fetchPaymentMethods(\n        { customerId: params.customerId },\n        setState,\n        {},\n      );\n    } catch (error) {\n      console.error(\"attachPaymentMethod error:\", error);\n    }\n  },\n\n  detachPaymentMethod: async (params, _setState) => {\n    try {\n      if (!params?.paymentMethodId) return;\n      await stripe.paymentMethods.detach(params.paymentMethodId as string);\n    } catch (error) {\n      console.error(\"detachPaymentMethod error:\", error);\n    }\n  },\n\n  setDefaultPaymentMethod: async (params, setState) => {\n    try {\n      if (!params?.customerId || !params?.paymentMethodId) return;\n      await stripe.customers.update(params.customerId as string, {\n        invoice_settings: {\n          default_payment_method: params.paymentMethodId as string,\n        },\n      });\n      await actionHandlers.fetchCustomers({}, setState, {});\n    } catch (error) {\n      console.error(\"setDefaultPaymentMethod error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Coupon & Promotion Actions\n  // ===========================================================================\n  fetchCoupons: async (params, setState) => {\n    try {\n      const coupons = await stripe.coupons.list({\n        limit: (params?.limit as number) ?? 10,\n      });\n\n      const data = coupons.data.map((c) => ({\n        id: c.id,\n        name: c.name,\n        percentOff: c.percent_off,\n        amountOff: c.amount_off,\n        currency: c.currency,\n        duration: c.duration,\n        durationInMonths: c.duration_in_months,\n        maxRedemptions: c.max_redemptions,\n        timesRedeemed: c.times_redeemed,\n        valid: c.valid,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        coupons: { data, total: coupons.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCoupons error:\", error);\n    }\n  },\n\n  createCoupon: async (params, setState) => {\n    try {\n      await stripe.coupons.create({\n        percent_off: (params?.percentOff as number) ?? undefined,\n        amount_off: (params?.amountOff as number) ?? undefined,\n        currency: (params?.currency as string) ?? undefined,\n        duration:\n          (params?.duration as \"forever\" | \"once\" | \"repeating\") ?? \"once\",\n        duration_in_months: (params?.durationInMonths as number) ?? undefined,\n        name: (params?.name as string) ?? undefined,\n        max_redemptions: (params?.maxRedemptions as number) ?? undefined,\n      });\n      await actionHandlers.fetchCoupons({}, setState, {});\n    } catch (error) {\n      console.error(\"createCoupon error:\", error);\n    }\n  },\n\n  deleteCoupon: async (params, setState) => {\n    try {\n      if (!params?.couponId) return;\n      await stripe.coupons.del(params.couponId as string);\n      await actionHandlers.fetchCoupons({}, setState, {});\n    } catch (error) {\n      console.error(\"deleteCoupon error:\", error);\n    }\n  },\n\n  fetchPromotionCodes: async (params, setState) => {\n    try {\n      const promoCodes = await stripe.promotionCodes.list({\n        limit: (params?.limit as number) ?? 10,\n        coupon: (params?.couponId as string) || undefined,\n        active: (params?.active as boolean) ?? undefined,\n      });\n\n      const data = promoCodes.data.map((p) => ({\n        id: p.id,\n        code: p.code,\n        couponId: typeof p.coupon === \"string\" ? p.coupon : p.coupon?.id,\n        active: p.active,\n        maxRedemptions: p.max_redemptions,\n        timesRedeemed: p.times_redeemed,\n        expiresAt: p.expires_at ? formatDate(p.expires_at) : null,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        promotionCodes: { data, total: promoCodes.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchPromotionCodes error:\", error);\n    }\n  },\n\n  createPromotionCode: async (params, setState) => {\n    try {\n      if (!params?.couponId) return;\n      await stripe.promotionCodes.create({\n        coupon: params.couponId as string,\n        code: (params?.code as string) ?? undefined,\n        max_redemptions: (params?.maxRedemptions as number) ?? undefined,\n        expires_at: (params?.expiresAt as number) ?? undefined,\n      });\n      await actionHandlers.fetchPromotionCodes({}, setState, {});\n    } catch (error) {\n      console.error(\"createPromotionCode error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Checkout Session Actions\n  // ===========================================================================\n  createCheckoutSession: async (params, setState) => {\n    try {\n      const lineItems =\n        (params?.lineItems as Array<{ priceId: string; quantity: number }>) ??\n        [];\n      const session = await stripe.checkout.sessions.create({\n        mode:\n          (params?.mode as \"payment\" | \"subscription\" | \"setup\") ?? \"payment\",\n        line_items: lineItems.map((li) => ({\n          price: li.priceId,\n          quantity: li.quantity,\n        })),\n        success_url: (params?.successUrl as string) ?? \"\",\n        cancel_url: (params?.cancelUrl as string) ?? \"\",\n        customer: (params?.customerId as string) ?? undefined,\n      });\n\n      setState((prev) => ({\n        ...prev,\n        checkoutSession: { id: session.id, url: session.url },\n      }));\n\n      if (session.url) {\n        window.open(session.url, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"createCheckoutSession error:\", error);\n    }\n  },\n\n  fetchCheckoutSessions: async (params, setState) => {\n    try {\n      const sessions = await stripe.checkout.sessions.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n        payment_intent: (params?.paymentIntentId as string) || undefined,\n      });\n\n      const data = sessions.data.map((s) => ({\n        id: s.id,\n        mode: s.mode,\n        status: s.status,\n        amountTotal: s.amount_total,\n        currency: s.currency,\n        customerEmail: s.customer_email,\n        url: s.url,\n        created: formatDate(s.created),\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        checkoutSessions: { data, total: sessions.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchCheckoutSessions error:\", error);\n    }\n  },\n\n  expireCheckoutSession: async (params, setState) => {\n    try {\n      if (!params?.sessionId) return;\n      await stripe.checkout.sessions.expire(params.sessionId as string);\n      await actionHandlers.fetchCheckoutSessions({}, setState, {});\n    } catch (error) {\n      console.error(\"expireCheckoutSession error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Billing Portal Actions\n  // ===========================================================================\n  createBillingPortalSession: async (params, setState) => {\n    try {\n      if (!params?.customerId) return;\n      const session = await stripe.billingPortal.sessions.create({\n        customer: params.customerId as string,\n        return_url: (params?.returnUrl as string) ?? window.location.href,\n      });\n\n      setState((prev) => ({\n        ...prev,\n        billingPortalSession: { url: session.url },\n      }));\n\n      if (session.url) {\n        window.open(session.url, \"_blank\");\n      }\n    } catch (error) {\n      console.error(\"createBillingPortalSession error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Event Actions\n  // ===========================================================================\n  fetchEvents: async (params, setState) => {\n    try {\n      const events = await stripe.events.list({\n        limit: (params?.limit as number) ?? 10,\n        type: (params?.type as string) || undefined,\n        created: {\n          gte: (params?.createdGte as number) || undefined,\n          lte: (params?.createdLte as number) || undefined,\n        },\n      });\n\n      const data = events.data.map((e) => ({\n        id: e.id,\n        type: e.type,\n        created: formatDate(e.created),\n        livemode: e.livemode,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        events: { data, total: events.data.length, hasMore: events.has_more },\n      }));\n    } catch (error) {\n      console.error(\"fetchEvents error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Setup Intent Actions\n  // ===========================================================================\n  createSetupIntent: async (params, setState) => {\n    try {\n      const setupIntent = await stripe.setupIntents.create({\n        customer: (params?.customerId as string) ?? undefined,\n        payment_method_types: (params?.paymentMethodTypes as string[]) ?? [\n          \"card\",\n        ],\n        usage: (params?.usage as \"on_session\" | \"off_session\") ?? \"off_session\",\n      });\n\n      setState((prev) => ({\n        ...prev,\n        setupIntent: {\n          id: setupIntent.id,\n          clientSecret: setupIntent.client_secret,\n        },\n      }));\n    } catch (error) {\n      console.error(\"createSetupIntent error:\", error);\n    }\n  },\n\n  fetchSetupIntents: async (params, setState) => {\n    try {\n      const setupIntents = await stripe.setupIntents.list({\n        limit: (params?.limit as number) ?? 10,\n        customer: (params?.customerId as string) || undefined,\n      });\n\n      const data = setupIntents.data.map((si) => ({\n        id: si.id,\n        status: si.status,\n        usage: si.usage,\n        created: formatDate(si.created),\n        paymentMethodTypes: si.payment_method_types,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        setupIntents: { data, total: setupIntents.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchSetupIntents error:\", error);\n    }\n  },\n\n  cancelSetupIntent: async (params, setState) => {\n    try {\n      if (!params?.setupIntentId) return;\n      await stripe.setupIntents.cancel(params.setupIntentId as string);\n      await actionHandlers.fetchSetupIntents({}, setState, {});\n    } catch (error) {\n      console.error(\"cancelSetupIntent error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Tax Rate Actions\n  // ===========================================================================\n  fetchTaxRates: async (params, setState) => {\n    try {\n      const taxRates = await stripe.taxRates.list({\n        limit: (params?.limit as number) ?? 10,\n        active: (params?.active as boolean) ?? undefined,\n        inclusive: (params?.inclusive as boolean) ?? undefined,\n      });\n\n      const data = taxRates.data.map((tr) => ({\n        id: tr.id,\n        displayName: tr.display_name,\n        percentage: tr.percentage,\n        inclusive: tr.inclusive,\n        jurisdiction: tr.jurisdiction,\n        description: tr.description,\n        active: tr.active,\n      }));\n\n      setState((prev) => ({\n        ...prev,\n        taxRates: { data, total: taxRates.data.length },\n      }));\n    } catch (error) {\n      console.error(\"fetchTaxRates error:\", error);\n    }\n  },\n\n  createTaxRate: async (params, setState) => {\n    try {\n      if (!params?.displayName || params?.percentage === undefined) return;\n      await stripe.taxRates.create({\n        display_name: params.displayName as string,\n        percentage: params.percentage as number,\n        inclusive: (params?.inclusive as boolean) ?? false,\n        jurisdiction: (params?.jurisdiction as string) ?? undefined,\n        description: (params?.description as string) ?? undefined,\n      });\n      await actionHandlers.fetchTaxRates({}, setState, {});\n    } catch (error) {\n      console.error(\"createTaxRate error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Data & Refresh Actions\n  // ===========================================================================\n  refreshData: async (_, setState) => {\n    await Promise.all([\n      actionHandlers.fetchCustomers({}, setState, {}),\n      actionHandlers.fetchPayments({}, setState, {}),\n      actionHandlers.fetchSubscriptions({}, setState, {}),\n      actionHandlers.fetchInvoices({}, setState, {}),\n    ]);\n  },\n\n  refreshCustomers: async (_, setState) => {\n    await actionHandlers.fetchCustomers({}, setState, {});\n  },\n\n  refreshPayments: async (_, setState) => {\n    await actionHandlers.fetchPayments({}, setState, {});\n  },\n\n  refreshSubscriptions: async (_, setState) => {\n    await actionHandlers.fetchSubscriptions({}, setState, {});\n  },\n\n  refreshInvoices: async (_, setState) => {\n    await actionHandlers.fetchInvoices({}, setState, {});\n  },\n\n  exportData: async (params, _, data) => {\n    try {\n      const format = (params?.format as string) ?? \"json\";\n      const dataType = (params?.dataType as string) ?? \"customers\";\n      const exportData = (data[dataType] as { data: unknown[] })?.data ?? [];\n\n      if (format === \"json\") {\n        const blob = new Blob([JSON.stringify(exportData, null, 2)], {\n          type: \"application/json\",\n        });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = `${dataType}.json`;\n        a.click();\n        URL.revokeObjectURL(url);\n      } else if (format === \"csv\") {\n        if (exportData.length === 0) return;\n        const headers = Object.keys(exportData[0] as object);\n        const csv = [\n          headers.join(\",\"),\n          ...exportData.map((row) =>\n            headers\n              .map((h) =>\n                JSON.stringify((row as Record<string, unknown>)[h] ?? \"\"),\n              )\n              .join(\",\"),\n          ),\n        ].join(\"\\n\");\n        const blob = new Blob([csv], { type: \"text/csv\" });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement(\"a\");\n        a.href = url;\n        a.download = `${dataType}.csv`;\n        a.click();\n        URL.revokeObjectURL(url);\n      }\n    } catch (error) {\n      console.error(\"exportData error:\", error);\n    }\n  },\n\n  // ===========================================================================\n  // Navigation Actions\n  // ===========================================================================\n  navigate: async (_params) => {\n    // In a real app, this would use the router\n  },\n\n  openDashboard: async (params) => {\n    const page = (params?.page as string) ?? \"home\";\n    const paths: Record<string, string> = {\n      home: \"\",\n      payments: \"payments\",\n      customers: \"customers\",\n      products: \"products\",\n      subscriptions: \"subscriptions\",\n      invoices: \"invoices\",\n      connect: \"connect/accounts\",\n      reports: \"reports\",\n      developers: \"developers\",\n    };\n    window.open(`https://dashboard.stripe.com/${paths[page] ?? \"\"}`, \"_blank\");\n  },\n\n  openExternalLink: async (params) => {\n    if (params?.url) {\n      window.open(params.url as string, \"_blank\");\n    }\n  },\n\n  // ===========================================================================\n  // Form Actions\n  // ===========================================================================\n  submitForm: async (_params, _, _data) => {\n    // Implementation depends on form handling logic\n  },\n\n  resetForm: async (params, setState) => {\n    const formId = params?.formId as string;\n    if (formId) {\n      setState((prev) => ({ ...prev, [formId]: {} }));\n    }\n  },\n\n  validateForm: async (_params, _, _data) => {\n    // Implementation depends on form validation logic\n  },\n\n  setFormValue: async (params, setState) => {\n    if (params?.statePath) {\n      setState((prev) => ({\n        ...prev,\n        [params.statePath as string]: params?.value,\n      }));\n    }\n  },\n\n  // ===========================================================================\n  // UI Actions\n  // ===========================================================================\n  showToast: async (_params) => {\n    // In a real app, use showToast from @stripe/ui-extension-sdk/utils\n  },\n\n  copyToClipboard: async (params) => {\n    if (params?.text) {\n      await navigator.clipboard.writeText(params.text as string);\n    }\n  },\n\n  setLoading: async (params, setState) => {\n    setState((prev) => ({\n      ...prev,\n      loading: params?.loading,\n      loadingMessage: params?.message,\n    }));\n  },\n\n  // ===========================================================================\n  // Filter & Sort Actions\n  // ===========================================================================\n  setFilter: async (params, setState) => {\n    if (params?.key) {\n      setState((prev) => ({\n        ...prev,\n        filters: {\n          ...(prev.filters as Record<string, unknown>),\n          [params.key as string]: params?.value,\n        },\n      }));\n    }\n  },\n\n  clearFilters: async (_, setState) => {\n    setState((prev) => ({ ...prev, filters: {} }));\n  },\n\n  setSort: async (params, setState) => {\n    setState((prev) => ({\n      ...prev,\n      sort: { field: params?.field, direction: params?.direction },\n    }));\n  },\n\n  setPageSize: async (params, setState) => {\n    setState((prev) => ({ ...prev, pageSize: params?.size }));\n  },\n\n  goToPage: async (params, setState) => {\n    setState((prev) => ({ ...prev, currentPage: params?.page }));\n  },\n};\n\n// =============================================================================\n// Execute Action\n// =============================================================================\n\ntype SetStateFn = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n/**\n * Execute an action by name with the given parameters.\n */\nexport async function executeAction(\n  actionName: string,\n  params: Record<string, unknown> | undefined,\n  setState: SetStateFn,\n  data: Record<string, unknown> = {},\n): Promise<void> {\n  const handler = actionHandlers[actionName];\n  if (handler) {\n    await handler(params, setState, data);\n  } else {\n    console.warn(\"Unknown action:\", actionName);\n  }\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/render/catalog/components.tsx",
    "content": "/**\n * Stripe UIXT Component Implementations\n *\n * Maps json-render catalog components to Stripe UI Extension SDK components.\n */\nimport type { FunctionComponent } from \"react\";\nimport type { ComponentRenderProps } from \"@json-render/react\";\nimport {\n  Box,\n  Badge as UIBadge,\n  Banner as UIBanner,\n  Button as UIButton,\n  Checkbox as UICheckbox,\n  DateField as UIDateField,\n  Divider as UIDivider,\n  Icon as UIIcon,\n  Img as UIImg,\n  Inline as UIInline,\n  Link as UILink,\n  List as UIList,\n  ListItem as UIListItem,\n  Menu as UIMenu,\n  MenuGroup as UIMenuGroup,\n  MenuItem as UIMenuItem,\n  PropertyList as UIPropertyList,\n  PropertyListItem as UIPropertyListItem,\n  Radio as UIRadio,\n  Select as UISelect,\n  Spinner as UISpinner,\n  Switch as UISwitch,\n  TaskList as UITaskList,\n  TaskListItem as UITaskListItem,\n  TextArea as UITextArea,\n  TextField as UITextField,\n  Tooltip as UITooltip,\n  Accordion as UIAccordion,\n  AccordionItem as UIAccordionItem,\n  BarChart as UIBarChart,\n  LineChart as UILineChart,\n  Sparkline as UISparkline,\n  ButtonGroup as UIButtonGroup,\n  Chip as UIChip,\n  ChipList as UIChipList,\n  Table,\n  TableHead,\n  TableBody,\n  TableRow,\n  TableHeaderCell,\n  TableCell,\n} from \"@stripe/ui-extension-sdk/ui\";\n\n// Helper to format currency\nconst formatCurrency = (amount: number, currency = \"usd\"): string => {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: currency.toUpperCase(),\n  }).format(amount / 100);\n};\n\n// Extended props type for components with data access\ntype ExtendedRenderProps<P = Record<string, unknown>> =\n  ComponentRenderProps<P> & {\n    state?: Record<string, unknown>;\n    getValue?: (path: string) => unknown;\n    onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  };\n\n// NOTE: All interactive components use `emit` to fire named events.\n// The renderer resolves events to action bindings from the element's `on` field.\n\n// =========================================================================\n// Layout Components\n// =========================================================================\nexport const Stack: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const {\n    direction = \"vertical\",\n    gap = \"medium\",\n    distribute,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: direction === \"horizontal\" ? \"x\" : \"y\",\n        gap: gap as \"xsmall\" | \"small\" | \"medium\" | \"large\" | \"xlarge\",\n        ...(distribute === \"space-between\" && {\n          distribute: \"space-between\" as const,\n        }),\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nexport const Inline: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIInline>{children}</UIInline>;\n};\n\nexport const Divider: FunctionComponent<ExtendedRenderProps> = () => {\n  return <UIDivider />;\n};\n\nexport const Accordion: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIAccordion>{children}</UIAccordion>;\n};\n\nexport const AccordionItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { title, subtitle, defaultOpen } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIAccordionItem\n      title={String(title || \"\")}\n      subtitle={subtitle ? String(subtitle) : undefined}\n      defaultOpen={Boolean(defaultOpen) || undefined}\n    >\n      {children}\n    </UIAccordionItem>\n  );\n};\n\n// =========================================================================\n// Typography Components\n// =========================================================================\nexport const Heading: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { text, size = \"large\" } = element.props as Record<string, unknown>;\n\n  const fontMap: Record<\n    string,\n    \"heading\" | \"title\" | \"subtitle\" | \"subheading\" | \"body\"\n  > = {\n    xsmall: \"body\",\n    small: \"subheading\",\n    medium: \"subtitle\",\n    large: \"title\",\n    xlarge: \"heading\",\n  };\n\n  return (\n    <Box css={{ font: fontMap[size as string] || \"title\", fontWeight: \"bold\" }}>\n      {String(text || \"\")}\n    </Box>\n  );\n};\n\nexport const Text: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    content,\n    color = \"primary\",\n    size = \"medium\",\n    weight = \"regular\",\n  } = element.props as Record<string, unknown>;\n\n  const colorMap: Record<\n    string,\n    | \"primary\"\n    | \"secondary\"\n    | \"disabled\"\n    | \"critical\"\n    | \"success\"\n    | \"attention\"\n    | \"info\"\n  > = {\n    primary: \"primary\",\n    secondary: \"secondary\",\n    disabled: \"disabled\",\n    critical: \"critical\",\n    success: \"success\",\n    warning: \"attention\",\n    info: \"info\",\n  };\n\n  const fontMap: Record<string, \"caption\" | \"body\" | \"subtitle\" | \"title\"> = {\n    xsmall: \"caption\",\n    small: \"caption\",\n    medium: \"body\",\n    large: \"subtitle\",\n  };\n\n  const weightMap: Record<string, \"regular\" | \"semibold\" | \"bold\"> = {\n    regular: \"regular\",\n    medium: \"regular\",\n    semibold: \"semibold\",\n    bold: \"bold\",\n  };\n\n  return (\n    <Box\n      css={{\n        font: fontMap[size as string] || \"body\",\n        color: colorMap[color as string] || \"primary\",\n        fontWeight: weightMap[weight as string] || \"regular\",\n      }}\n    >\n      {String(content || \"\")}\n    </Box>\n  );\n};\n\n// =========================================================================\n// Data Display Components\n// =========================================================================\nexport const Metric: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    value,\n    change,\n    changeType = \"neutral\",\n  } = element.props as Record<string, unknown>;\n\n  const changeColors: Record<string, \"success\" | \"critical\" | \"secondary\"> = {\n    positive: \"success\",\n    negative: \"critical\",\n    neutral: \"secondary\",\n  };\n\n  return (\n    <Box css={{ stack: \"y\", gap: \"xsmall\" }}>\n      <Box css={{ font: \"caption\", color: \"secondary\" }}>\n        {String(label || \"\")}\n      </Box>\n      <Box css={{ font: \"heading\", fontWeight: \"bold\" }}>\n        {String(value || \"\")}\n      </Box>\n      {change && (\n        <Box\n          css={{\n            font: \"caption\",\n            color: changeColors[changeType as string] || \"secondary\",\n          }}\n        >\n          {changeType === \"positive\"\n            ? \"↑\"\n            : changeType === \"negative\"\n              ? \"↓\"\n              : \"\"}{\" \"}\n          {String(change)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nconst BADGE_TYPES = new Set([\n  \"neutral\",\n  \"urgent\",\n  \"warning\",\n  \"negative\",\n  \"positive\",\n  \"info\",\n]);\nconst BADGE_ALIAS: Record<string, string> = {\n  success: \"positive\",\n  error: \"negative\",\n  danger: \"negative\",\n  critical: \"urgent\",\n  default: \"neutral\",\n  primary: \"info\",\n};\n\nfunction coerceBadgeType(\n  raw: unknown,\n): \"neutral\" | \"urgent\" | \"warning\" | \"negative\" | \"positive\" | \"info\" {\n  const s = String(raw ?? \"neutral\");\n  if (BADGE_TYPES.has(s))\n    return s as\n      | \"neutral\"\n      | \"urgent\"\n      | \"warning\"\n      | \"negative\"\n      | \"positive\"\n      | \"info\";\n  return (BADGE_ALIAS[s] ?? \"neutral\") as\n    | \"neutral\"\n    | \"urgent\"\n    | \"warning\"\n    | \"negative\"\n    | \"positive\"\n    | \"info\";\n}\n\nexport const Badge: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { label, type = \"neutral\" } = element.props as Record<string, unknown>;\n  return <UIBadge type={coerceBadgeType(type)}>{String(label || \"\")}</UIBadge>;\n};\n\nexport const Icon: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { name, size = \"medium\" } = element.props as Record<string, unknown>;\n  const sizeMap: Record<string, \"xsmall\" | \"small\" | \"medium\" | \"large\"> = {\n    xsmall: \"xsmall\",\n    small: \"small\",\n    medium: \"medium\",\n    large: \"large\",\n  };\n  return (\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe UIXT Icon name type is complex\n    <UIIcon name={name as any} size={sizeMap[size as string] || \"medium\"} />\n  );\n};\n\nexport const Img: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { src, alt, width, height } = element.props as Record<string, unknown>;\n  return (\n    <UIImg\n      src={String(src || \"\")}\n      alt={String(alt || \"\")}\n      width={width as number | undefined}\n      height={height as number | undefined}\n    />\n  );\n};\n\nexport const Spinner: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { size = \"medium\" } = element.props as Record<string, unknown>;\n  return <UISpinner size={size as \"small\" | \"medium\" | \"large\"} />;\n};\n\n// =========================================================================\n// Feedback Components\n// =========================================================================\nconst BANNER_TYPES = new Set([\"default\", \"caution\", \"critical\"]);\nconst BANNER_ALIAS: Record<string, string> = {\n  info: \"default\",\n  warning: \"caution\",\n  error: \"critical\",\n  danger: \"critical\",\n  success: \"default\",\n};\n\nfunction coerceBannerType(raw: unknown): \"default\" | \"caution\" | \"critical\" {\n  const s = String(raw ?? \"default\");\n  if (BANNER_TYPES.has(s)) return s as \"default\" | \"caution\" | \"critical\";\n  return (BANNER_ALIAS[s] ?? \"default\") as \"default\" | \"caution\" | \"critical\";\n}\n\nexport const Banner: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    title,\n    description,\n    type = \"default\",\n  } = element.props as Record<string, unknown>;\n  return (\n    <UIBanner\n      title={title ? String(title) : undefined}\n      description={description ? String(description) : undefined}\n      type={coerceBannerType(type)}\n    />\n  );\n};\n\n// =========================================================================\n// List Components\n// =========================================================================\nexport const List: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n  emit,\n}) => {\n  return <UIList onAction={(_id) => emit(\"select\")}>{children}</UIList>;\n};\n\nexport const ListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    title,\n    secondaryTitle,\n    value,\n    id,\n    size = \"default\",\n  } = element.props as Record<string, unknown>;\n  return (\n    <UIListItem\n      title={<Box>{String(title || \"\")}</Box>}\n      secondaryTitle={\n        secondaryTitle ? <Box>{String(secondaryTitle)}</Box> : undefined\n      }\n      value={value ? <Box>{String(value)}</Box> : undefined}\n      id={id ? String(id) : undefined}\n      size={size as \"default\" | \"large\"}\n    />\n  );\n};\n\nexport const PropertyList: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { orientation = \"vertical\" } = element.props as Record<string, unknown>;\n  return (\n    <UIPropertyList orientation={orientation as \"vertical\" | \"horizontal\"}>\n      {children}\n    </UIPropertyList>\n  );\n};\n\nexport const PropertyListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { label, value } = element.props as Record<string, unknown>;\n  return (\n    <UIPropertyListItem\n      label={String(label || \"\")}\n      value={String(value || \"\")}\n    />\n  );\n};\n\nexport const TaskList: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UITaskList>{children}</UITaskList>;\n};\n\nexport const TaskListItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    title,\n    status = \"not-started\",\n    action,\n  } = element.props as Record<string, unknown>;\n  return (\n    <UITaskListItem\n      title={String(title || \"\")}\n      status={status as \"not-started\" | \"in-progress\" | \"blocked\" | \"complete\"}\n      onPress={action ? () => emit(\"press\") : undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Menu Components\n// =========================================================================\nexport const Menu: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n  emit,\n}) => {\n  const { triggerLabel } = element.props as Record<string, unknown>;\n  return (\n    <UIMenu\n      trigger={\n        <UIButton type=\"secondary\">{String(triggerLabel || \"Menu\")}</UIButton>\n      }\n      onAction={(_id) => emit(\"select\")}\n    >\n      {children}\n    </UIMenu>\n  );\n};\n\nexport const MenuItem: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const { label, id, disabled, action } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIMenuItem\n      id={String(id || \"\")}\n      disabled={Boolean(disabled) || undefined}\n      onAction={action ? () => emit(\"select\") : undefined}\n    >\n      {String(label || \"\")}\n    </UIMenuItem>\n  );\n};\n\nexport const MenuGroup: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIMenuGroup>{children}</UIMenuGroup>;\n};\n\n// =========================================================================\n// Form Components\n// =========================================================================\nexport const TextField: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    placeholder,\n    description,\n    error,\n    value = \"\",\n    size = \"medium\",\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UITextField\n      label={String(label || \"\")}\n      placeholder={placeholder ? String(placeholder) : undefined}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const TextArea: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    placeholder,\n    description,\n    error,\n    value = \"\",\n    rows = 3,\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UITextArea\n      label={String(label || \"\")}\n      placeholder={placeholder ? String(placeholder) : undefined}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      rows={Number(rows)}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Select: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    description,\n    error,\n    value = \"\",\n    options = [],\n    size = \"medium\",\n    disabled,\n    required,\n  } = element.props as Record<string, unknown>;\n  const opts = options as Array<{ value: string; label: string }>;\n\n  return (\n    <UISelect\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      required={Boolean(required) || undefined}\n      onChange={() => undefined}\n    >\n      {opts.map((opt: { value: string; label: string }) => (\n        <option key={opt.value} value={opt.value}>\n          {opt.label}\n        </option>\n      ))}\n    </UISelect>\n  );\n};\n\nexport const Checkbox: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const { label, description, error, checked, defaultChecked, disabled } =\n    element.props as Record<string, unknown>;\n  const isChecked = checked ?? defaultChecked;\n\n  return (\n    <UICheckbox\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      checked={Boolean(isChecked) || undefined}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Radio: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    description,\n    value: selectedValue,\n    optionValue,\n    name,\n    disabled,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UIRadio\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      value={String(optionValue ?? \"\")}\n      name={String(name || \"\")}\n      checked={String(selectedValue ?? \"\") === String(optionValue ?? \"\")}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const Switch: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const { label, description, checked, defaultChecked, disabled } =\n    element.props as Record<string, unknown>;\n  const isChecked = checked ?? defaultChecked;\n\n  return (\n    <UISwitch\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      checked={Boolean(isChecked) || undefined}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\nexport const DateField: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    label,\n    description,\n    error,\n    value = \"\",\n    size = \"medium\",\n    disabled,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <UIDateField\n      label={String(label || \"\")}\n      description={description ? String(description) : undefined}\n      error={error ? String(error) : undefined}\n      value={String(value ?? \"\")}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      onChange={() => undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Button Components\n// =========================================================================\nexport const Button: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n  onAction,\n}) => {\n  const {\n    label,\n    action,\n    actionParams,\n    type = \"primary\",\n    size = \"medium\",\n    disabled,\n    pending,\n    href,\n  } = element.props as Record<string, unknown>;\n\n  const handlePress = action\n    ? () => {\n        emit(\"press\");\n        if (onAction) {\n          onAction(\n            String(action),\n            actionParams as Record<string, unknown> | undefined,\n          );\n        }\n      }\n    : undefined;\n\n  return (\n    <UIButton\n      type={type as \"primary\" | \"secondary\" | \"destructive\"}\n      size={size as \"small\" | \"medium\" | \"large\"}\n      disabled={Boolean(disabled) || undefined}\n      pending={Boolean(pending) || undefined}\n      href={href ? String(href) : undefined}\n      onPress={handlePress}\n    >\n      {String(label || \"\")}\n    </UIButton>\n  );\n};\n\nexport const ButtonGroup: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIButtonGroup>{children}</UIButtonGroup>;\n};\n\nexport const Link: FunctionComponent<ExtendedRenderProps> = ({ element }) => {\n  const {\n    label,\n    href,\n    type = \"primary\",\n    external,\n  } = element.props as Record<string, unknown>;\n  return (\n    <UILink\n      href={String(href || \"\")}\n      type={type as \"primary\" | \"secondary\"}\n      external={Boolean(external) || undefined}\n    >\n      {String(label || \"\")}\n    </UILink>\n  );\n};\n\n// =========================================================================\n// Chart Components\n// =========================================================================\nexport const BarChart: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    colorKey,\n    showAxis = \"both\",\n    showGrid = \"none\",\n    showLegend,\n    showTooltip = true,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return (\n      <Box css={{ color: \"secondary\", font: \"caption\" }}>No data available</Box>\n    );\n  }\n\n  return (\n    <UIBarChart\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      color={colorKey ? String(colorKey) : undefined}\n      axis={coerceAxisGrid(showAxis, \"both\")}\n      grid={coerceAxisGrid(showGrid, \"none\")}\n      legend={Boolean(showLegend) || undefined}\n      tooltip={Boolean(showTooltip)}\n    />\n  );\n};\n\nfunction coerceAxisGrid(\n  val: unknown,\n  fallback: string,\n): \"x\" | \"y\" | \"both\" | \"none\" {\n  if (val === true) return \"both\";\n  if (val === false) return \"none\";\n  const s = String(val ?? fallback);\n  if ([\"x\", \"y\", \"both\", \"none\"].includes(s))\n    return s as \"x\" | \"y\" | \"both\" | \"none\";\n  return fallback as \"x\" | \"y\" | \"both\" | \"none\";\n}\n\nexport const LineChart: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    colorKey,\n    showAxis = \"both\",\n    showGrid = \"none\",\n    showLegend,\n    showTooltip = true,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return (\n      <Box css={{ color: \"secondary\", font: \"caption\" }}>No data available</Box>\n    );\n  }\n\n  return (\n    <UILineChart\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      color={colorKey ? String(colorKey) : undefined}\n      axis={coerceAxisGrid(showAxis, \"both\")}\n      grid={coerceAxisGrid(showGrid, \"none\")}\n      legend={Boolean(showLegend) || undefined}\n      tooltip={Boolean(showTooltip)}\n    />\n  );\n};\n\nexport const Sparkline: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    data = [],\n    xKey,\n    yKey,\n    showTooltip,\n  } = element.props as Record<string, unknown>;\n\n  const chartData = Array.isArray(data) ? data : [];\n\n  if (!chartData || !Array.isArray(chartData) || chartData.length === 0) {\n    return <Box css={{ color: \"secondary\", font: \"caption\" }}>—</Box>;\n  }\n\n  return (\n    <UISparkline\n      data={chartData}\n      x={String(xKey || \"x\")}\n      y={String(yKey || \"y\")}\n      tooltip={Boolean(showTooltip) || undefined}\n    />\n  );\n};\n\n// =========================================================================\n// Table Component\n// =========================================================================\nexport const DataTable: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    title,\n    data = [],\n    columns = [],\n    emptyMessage,\n  } = element.props as Record<string, unknown>;\n\n  const tableData = Array.isArray(data) ? data : [];\n  const cols = columns as Array<{ key: string; label: string }>;\n\n  if (!tableData || tableData.length === 0) {\n    return (\n      <Box css={{ stack: \"y\", gap: \"small\" }}>\n        {title && (\n          <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n            {String(title)}\n          </Box>\n        )}\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          {String(emptyMessage || \"No data\")}\n        </Box>\n      </Box>\n    );\n  }\n\n  return (\n    <Box css={{ stack: \"y\", gap: \"small\" }}>\n      {title && (\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(title)}\n        </Box>\n      )}\n      <Table>\n        <TableHead>\n          <TableRow>\n            {cols.map((col: { key: string; label: string }) => (\n              <TableHeaderCell key={col.key}>{col.label}</TableHeaderCell>\n            ))}\n          </TableRow>\n        </TableHead>\n        <TableBody>\n          {tableData.map((row, idx) => (\n            <TableRow key={idx}>\n              {cols.map((col: { key: string; label: string }) => (\n                <TableCell key={col.key}>\n                  {String(row[col.key] ?? \"\")}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n        </TableBody>\n      </Table>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Stripe-Specific Card Components\n// =========================================================================\nexport const CustomerCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    name,\n    email,\n    status = \"active\",\n    customerId,\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(name || \"\")}\n        </Box>\n        <UIBadge type={status === \"active\" ? \"positive\" : \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ color: \"secondary\" }}>{String(email || \"\")}</Box>\n      {customerId && (\n        <UIButton type=\"secondary\" size=\"small\" onPress={() => emit(\"press\")}>\n          View Details\n        </UIButton>\n      )}\n    </Box>\n  );\n};\n\nexport const PaymentCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"succeeded\",\n    description,\n    paymentId,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    succeeded: \"positive\",\n    pending: \"warning\",\n    failed: \"negative\",\n    canceled: \"neutral\",\n    requires_action: \"info\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      {description && (\n        <Box css={{ color: \"secondary\" }}>{String(description)}</Box>\n      )}\n      {paymentId && (\n        <Box css={{ stack: \"x\", gap: \"small\" }}>\n          <UIButton type=\"secondary\" size=\"small\" onPress={() => emit(\"press\")}>\n            View\n          </UIButton>\n          <UIButton\n            type=\"destructive\"\n            size=\"small\"\n            onPress={() => emit(\"press\")}\n          >\n            Refund\n          </UIButton>\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const SubscriptionCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    planName,\n    status = \"active\",\n    amount,\n    currency = \"usd\",\n    interval = \"month\",\n    currentPeriodEnd,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    active: \"positive\",\n    trialing: \"info\",\n    past_due: \"warning\",\n    canceled: \"neutral\",\n    unpaid: \"negative\",\n    incomplete: \"warning\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(planName || \"\")}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n        {formatCurrency(Number(amount) || 0, String(currency))}/\n        {String(interval)}\n      </Box>\n      {currentPeriodEnd && (\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          Renews: {String(currentPeriodEnd)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const InvoiceCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const {\n    invoiceNumber,\n    amount,\n    currency = \"usd\",\n    status = \"open\",\n    dueDate,\n    customerEmail,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"info\"\n  > = {\n    draft: \"neutral\",\n    open: \"info\",\n    paid: \"positive\",\n    void: \"neutral\",\n    uncollectible: \"negative\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>\n          {String(invoiceNumber || \"\")}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n        {formatCurrency(Number(amount) || 0, String(currency))}\n      </Box>\n      {customerEmail && (\n        <Box css={{ color: \"secondary\" }}>{String(customerEmail)}</Box>\n      )}\n      {dueDate && (\n        <Box css={{ color: \"secondary\", font: \"caption\" }}>\n          Due: {String(dueDate)}\n        </Box>\n      )}\n      {status === \"open\" && (\n        <UIButton type=\"primary\" size=\"small\" onPress={() => emit(\"press\")}>\n          Send Invoice\n        </UIButton>\n      )}\n    </Box>\n  );\n};\n\nexport const RefundCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"succeeded\",\n    reason,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\"\n  > = {\n    pending: \"warning\",\n    succeeded: \"positive\",\n    failed: \"negative\",\n    canceled: \"neutral\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"neutral\"}>\n          {String(status)}\n        </UIBadge>\n      </Box>\n      {reason && <Box css={{ color: \"secondary\" }}>{String(reason)}</Box>}\n    </Box>\n  );\n};\n\nexport const DisputeCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    amount,\n    currency = \"usd\",\n    status = \"needs_response\",\n    reason,\n    dueDate,\n  } = element.props as Record<string, unknown>;\n\n  const statusColors: Record<\n    string,\n    \"positive\" | \"warning\" | \"negative\" | \"neutral\" | \"urgent\" | \"info\"\n  > = {\n    warning_needs_response: \"urgent\",\n    warning_under_review: \"warning\",\n    warning_closed: \"neutral\",\n    needs_response: \"urgent\",\n    under_review: \"warning\",\n    won: \"positive\",\n    lost: \"negative\",\n  };\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"critical\",\n      }}\n    >\n      <Box css={{ stack: \"x\", distribute: \"space-between\", alignY: \"center\" }}>\n        <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n          {formatCurrency(Number(amount) || 0, String(currency))}\n        </Box>\n        <UIBadge type={statusColors[String(status)] || \"warning\"}>\n          {String(status).replace(/_/g, \" \")}\n        </UIBadge>\n      </Box>\n      {reason && <Box css={{ color: \"secondary\" }}>{String(reason)}</Box>}\n      {dueDate && (\n        <Box css={{ color: \"critical\", font: \"caption\" }}>\n          Response due: {String(dueDate)}\n        </Box>\n      )}\n    </Box>\n  );\n};\n\nexport const BalanceCard: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  const {\n    available,\n    pending,\n    currency = \"usd\",\n  } = element.props as Record<string, unknown>;\n\n  return (\n    <Box\n      css={{\n        stack: \"y\",\n        gap: \"small\",\n        padding: \"medium\",\n        borderRadius: \"medium\",\n        keyline: \"neutral\",\n      }}\n    >\n      <Box css={{ font: \"subtitle\", fontWeight: \"semibold\" }}>Balance</Box>\n      <Box css={{ stack: \"x\", distribute: \"space-between\" }}>\n        <Box>\n          <Box css={{ font: \"caption\", color: \"secondary\" }}>Available</Box>\n          <Box css={{ font: \"title\", fontWeight: \"bold\", color: \"success\" }}>\n            {formatCurrency(Number(available) || 0, String(currency))}\n          </Box>\n        </Box>\n        <Box>\n          <Box css={{ font: \"caption\", color: \"secondary\" }}>Pending</Box>\n          <Box css={{ font: \"title\", fontWeight: \"bold\" }}>\n            {formatCurrency(Number(pending) || 0, String(currency))}\n          </Box>\n        </Box>\n      </Box>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Chip Components\n// =========================================================================\nexport const Chip: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  emit,\n}) => {\n  const { label, value, removable, action } = element.props as Record<\n    string,\n    unknown\n  >;\n  return (\n    <UIChip\n      label={String(label || \"\")}\n      value={value ? String(value) : undefined}\n      onClose={removable && action ? () => emit(\"remove\") : undefined}\n    />\n  );\n};\n\nexport const ChipList: FunctionComponent<ExtendedRenderProps> = ({\n  children,\n}) => {\n  return <UIChipList>{children}</UIChipList>;\n};\n\n// =========================================================================\n// Tooltip Component\n// =========================================================================\nexport const Tooltip: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n  children,\n}) => {\n  const { content, placement = \"top\" } = element.props as Record<\n    string,\n    unknown\n  >;\n\n  // Tooltip needs a trigger element - use children if available\n  if (!children) {\n    return <Box>{String(content || \"\")}</Box>;\n  }\n\n  return (\n    <UITooltip\n      trigger={<Box>{children}</Box>}\n      placement={placement as \"top\" | \"bottom\" | \"left\" | \"right\"}\n    >\n      <Box>{String(content || \"\")}</Box>\n    </UITooltip>\n  );\n};\n\n// =========================================================================\n// Fallback Component\n// =========================================================================\nexport const Fallback: FunctionComponent<ExtendedRenderProps> = ({\n  element,\n}) => {\n  return (\n    <Box css={{ padding: \"small\", keyline: \"critical\", borderRadius: \"small\" }}>\n      <Box css={{ color: \"critical\", font: \"caption\" }}>\n        Unknown component: {element.type}\n      </Box>\n    </Box>\n  );\n};\n\n// =========================================================================\n// Component Map Export\n// =========================================================================\nexport const components: Record<\n  string,\n  FunctionComponent<ExtendedRenderProps>\n> = {\n  // Layout\n  Stack,\n  Inline,\n  Divider,\n  Accordion,\n  AccordionItem,\n  // Typography\n  Heading,\n  Text,\n  // Data Display\n  Metric,\n  Badge,\n  Icon,\n  Img,\n  Spinner,\n  // Feedback\n  Banner,\n  // Lists\n  List,\n  ListItem,\n  PropertyList,\n  PropertyListItem,\n  TaskList,\n  TaskListItem,\n  // Menus\n  Menu,\n  MenuItem,\n  MenuGroup,\n  // Forms\n  TextField,\n  TextArea,\n  Select,\n  Checkbox,\n  Radio,\n  Switch,\n  DateField,\n  // Buttons\n  Button,\n  ButtonGroup,\n  Link,\n  // Charts\n  BarChart,\n  LineChart,\n  Sparkline,\n  // Tables\n  DataTable,\n  // Stripe Cards\n  CustomerCard,\n  PaymentCard,\n  SubscriptionCard,\n  InvoiceCard,\n  RefundCard,\n  DisputeCard,\n  BalanceCard,\n  // Chips\n  Chip,\n  ChipList,\n  // Tooltip\n  Tooltip,\n  // Fallback\n  Fallback,\n};\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/render/catalog.ts",
    "content": "import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\n/**\n * Stripe UIXT Catalog\n *\n * Comprehensive catalog mapping to Stripe UI Extension SDK components.\n */\nexport const stripeCatalog = defineCatalog(schema, {\n  components: {\n    // =========================================================================\n    // Layout Components\n    // =========================================================================\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).default(\"vertical\"),\n        gap: z\n          .enum([\"xsmall\", \"small\", \"medium\", \"large\", \"xlarge\"])\n          .default(\"medium\"),\n        alignX: z.enum([\"start\", \"center\", \"end\", \"stretch\"]).nullable(),\n        alignY: z\n          .enum([\"top\", \"center\", \"baseline\", \"bottom\", \"stretch\"])\n          .nullable(),\n        distribute: z.enum([\"space-between\", \"packed\"]).nullable(),\n      }),\n      description:\n        \"Flex layout container for arranging children horizontally or vertically with configurable gap and alignment\",\n      example: { direction: \"vertical\", gap: \"medium\" },\n    },\n\n    Inline: {\n      props: z.object({\n        gap: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"small\"),\n      }),\n      description:\n        \"Inline layout for text and small elements that wrap naturally\",\n    },\n\n    Divider: {\n      props: z.object({}),\n      description:\n        \"Visual horizontal divider line to separate content sections\",\n    },\n\n    Accordion: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description:\n        \"Collapsible accordion container for expandable content sections\",\n    },\n\n    AccordionItem: {\n      props: z.object({\n        title: z.string(),\n        subtitle: z.string().nullable(),\n        defaultOpen: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Individual accordion item with title, optional subtitle, and collapsible content\",\n    },\n\n    // =========================================================================\n    // Typography Components\n    // =========================================================================\n    Heading: {\n      props: z.object({\n        text: z.string(),\n        size: z\n          .enum([\"xsmall\", \"small\", \"medium\", \"large\", \"xlarge\"])\n          .default(\"large\"),\n      }),\n      description: \"Display a heading/title text with configurable size\",\n      example: { text: \"Overview\", size: \"large\" },\n    },\n\n    Text: {\n      props: z.object({\n        content: z.string(),\n        color: z\n          .enum([\n            \"primary\",\n            \"secondary\",\n            \"disabled\",\n            \"critical\",\n            \"success\",\n            \"warning\",\n            \"info\",\n          ])\n          .default(\"primary\"),\n        size: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"medium\"),\n        weight: z\n          .enum([\"regular\", \"medium\", \"semibold\", \"bold\"])\n          .default(\"regular\"),\n      }),\n      description:\n        \"Display body text with configurable color, size, and weight\",\n      example: { content: \"Payment received successfully.\", color: \"primary\" },\n    },\n\n    // =========================================================================\n    // Data Display Components\n    // =========================================================================\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        change: z.string().nullable(),\n        changeType: z\n          .enum([\"positive\", \"negative\", \"neutral\"])\n          .default(\"neutral\"),\n        format: z.enum([\"currency\", \"number\", \"percent\"]).nullable(),\n      }),\n      description:\n        \"Display a key metric with label, value, and optional trend indicator for KPIs\",\n      example: {\n        label: \"Revenue\",\n        value: \"$12,450\",\n        change: \"+8.2%\",\n        changeType: \"positive\",\n        format: \"currency\",\n      },\n    },\n\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        type: z\n          .enum([\n            \"neutral\",\n            \"urgent\",\n            \"warning\",\n            \"negative\",\n            \"positive\",\n            \"info\",\n          ])\n          .default(\"neutral\"),\n      }),\n      description: \"Status badge indicator with configurable color type\",\n      example: { label: \"Active\", type: \"positive\" },\n    },\n\n    Icon: {\n      props: z.object({\n        name: z.string(),\n        size: z.enum([\"xsmall\", \"small\", \"medium\", \"large\"]).default(\"medium\"),\n        color: z\n          .enum([\n            \"primary\",\n            \"secondary\",\n            \"disabled\",\n            \"critical\",\n            \"success\",\n            \"warning\",\n            \"info\",\n          ])\n          .nullable(),\n      }),\n      description:\n        \"Display an icon from Stripe's icon set (e.g., 'check', 'warning', 'customer', 'payment')\",\n    },\n\n    Img: {\n      props: z.object({\n        src: z.string(),\n        alt: z.string().nullable(),\n        width: z.number().nullable(),\n        height: z.number().nullable(),\n      }),\n      description: \"Display an image with configurable dimensions\",\n    },\n\n    Spinner: {\n      props: z.object({\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n      }),\n      description: \"Loading spinner indicator\",\n    },\n\n    // =========================================================================\n    // Feedback Components\n    // =========================================================================\n    Banner: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n        type: z.enum([\"default\", \"caution\", \"critical\"]).default(\"default\"),\n        dismissible: z.boolean().nullable(),\n      }),\n      description:\n        \"Alert banner for important messages with optional dismiss button\",\n    },\n\n    // =========================================================================\n    // List Components\n    // =========================================================================\n    List: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description:\n        \"Container for ListItem components with optional action handling\",\n    },\n\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        secondaryTitle: z.string().nullable(),\n        value: z.string().nullable(),\n        id: z.string().nullable(),\n        size: z.enum([\"default\", \"large\"]).default(\"default\"),\n      }),\n      description:\n        \"List item with title, optional secondary text and value display\",\n    },\n\n    PropertyList: {\n      props: z.object({\n        orientation: z.enum([\"vertical\", \"horizontal\"]).default(\"vertical\"),\n      }),\n      slots: [\"default\"],\n      description:\n        \"List of label-value pairs for displaying properties/details\",\n    },\n\n    PropertyListItem: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n      }),\n      description: \"Single property item with label and value\",\n    },\n\n    TaskList: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Container for task items with status indicators\",\n    },\n\n    TaskListItem: {\n      props: z.object({\n        title: z.string(),\n        status: z\n          .enum([\"not-started\", \"in-progress\", \"blocked\", \"complete\"])\n          .default(\"not-started\"),\n        action: z.string().nullable(),\n      }),\n      description: \"Task item with title and status indicator\",\n    },\n\n    // =========================================================================\n    // Menu Components\n    // =========================================================================\n    Menu: {\n      props: z.object({\n        triggerLabel: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Dropdown menu container with trigger button\",\n    },\n\n    MenuItem: {\n      props: z.object({\n        label: z.string(),\n        id: z.string(),\n        disabled: z.boolean().nullable(),\n        action: z.string().nullable(),\n      }),\n      description: \"Menu item with label and optional action\",\n    },\n\n    MenuGroup: {\n      props: z.object({\n        title: z.string().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Group of menu items with optional title\",\n    },\n\n    // =========================================================================\n    // Form Components\n    // =========================================================================\n    TextField: {\n      props: z.object({\n        label: z.string(),\n        placeholder: z.string().nullable(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        type: z\n          .enum([\"text\", \"email\", \"password\", \"number\", \"tel\", \"url\"])\n          .default(\"text\"),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Text input field with label, validation. Use $bindState on value for two-way binding.\",\n      example: {\n        label: \"Email\",\n        placeholder: \"customer@example.com\",\n        value: { $bindState: \"/form/email\" },\n        type: \"email\",\n      },\n    },\n\n    TextArea: {\n      props: z.object({\n        label: z.string(),\n        placeholder: z.string().nullable(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        rows: z.number().default(3),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Multi-line text input with configurable rows. Use $bindState on value for two-way binding.\",\n    },\n\n    Select: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        options: z.array(z.object({ value: z.string(), label: z.string() })),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        required: z.boolean().nullable(),\n      }),\n      description:\n        \"Dropdown select input with configurable options. Use $bindState on value for two-way binding.\",\n    },\n\n    Checkbox: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Checkbox input with label and description. Use $bindState on checked for two-way binding.\",\n    },\n\n    Radio: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        value: z.string().nullable(),\n        optionValue: z.string(),\n        name: z.string(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Radio button input for selecting one option from a group. Use $bindState on value for two-way binding.\",\n    },\n\n    Switch: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        checked: z.boolean().nullable(),\n        defaultChecked: z.boolean().nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Toggle switch for boolean values. Use $bindState on checked for two-way binding.\",\n    },\n\n    DateField: {\n      props: z.object({\n        label: z.string(),\n        description: z.string().nullable(),\n        error: z.string().nullable(),\n        value: z.string().nullable(),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Date input field with date picker. Use $bindState on value for two-way binding.\",\n    },\n\n    // =========================================================================\n    // Button Components\n    // =========================================================================\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n        actionParams: z.record(z.string(), z.unknown()).nullable(),\n        type: z\n          .enum([\"primary\", \"secondary\", \"destructive\"])\n          .default(\"primary\"),\n        size: z.enum([\"small\", \"medium\", \"large\"]).default(\"medium\"),\n        disabled: z.boolean().nullable(),\n        pending: z.boolean().nullable(),\n        href: z.string().nullable(),\n      }),\n      description:\n        \"Action button with configurable style, size, and action handling\",\n      example: {\n        label: \"View Details\",\n        action: \"viewCustomer\",\n        type: \"primary\",\n      },\n    },\n\n    ButtonGroup: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Group of buttons displayed together\",\n    },\n\n    Link: {\n      props: z.object({\n        label: z.string(),\n        href: z.string(),\n        type: z.enum([\"primary\", \"secondary\"]).default(\"primary\"),\n        external: z.boolean().nullable(),\n      }),\n      description: \"Text link for navigation\",\n    },\n\n    // =========================================================================\n    // Chart Components\n    // =========================================================================\n    BarChart: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        colorKey: z.string().nullable(),\n        showAxis: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"both\"),\n        showGrid: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"none\"),\n        showLegend: z.boolean().nullable(),\n        showTooltip: z.boolean().default(true),\n      }),\n      description:\n        \"Bar chart visualization. Use $state on data to bind to state array.\",\n    },\n\n    LineChart: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        colorKey: z.string().nullable(),\n        showAxis: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"both\"),\n        showGrid: z.enum([\"x\", \"y\", \"both\", \"none\"]).default(\"none\"),\n        showLegend: z.boolean().nullable(),\n        showTooltip: z.boolean().default(true),\n      }),\n      description:\n        \"Line chart visualization. Use $state on data to bind to state array.\",\n    },\n\n    Sparkline: {\n      props: z.object({\n        data: z.array(z.record(z.unknown())).nullable(),\n        xKey: z.string(),\n        yKey: z.string(),\n        showTooltip: z.boolean().nullable(),\n      }),\n      description:\n        \"Compact sparkline chart for inline data visualization. Use $state on data to bind to state array.\",\n    },\n\n    // =========================================================================\n    // Table Components\n    // =========================================================================\n    DataTable: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.unknown())).nullable(),\n        columns: z.array(z.object({ key: z.string(), label: z.string() })),\n        emptyMessage: z.string().nullable(),\n        rowAction: z.string().nullable(),\n      }),\n      description:\n        \"Data table with configurable columns and optional row actions. Use $state on data to bind to state array.\",\n      example: {\n        title: \"Recent Payments\",\n        data: { $state: \"/payments/data\" },\n        columns: [\n          { key: \"amount\", label: \"Amount\" },\n          { key: \"status\", label: \"Status\" },\n        ],\n      },\n    },\n\n    // =========================================================================\n    // Stripe-Specific Card Components\n    // =========================================================================\n    CustomerCard: {\n      props: z.object({\n        name: z.string(),\n        email: z.string(),\n        status: z.enum([\"active\", \"inactive\"]).default(\"active\"),\n        customerId: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying customer information with name, email, and status\",\n      example: {\n        name: \"Jane Smith\",\n        email: \"jane@example.com\",\n        status: \"active\",\n      },\n    },\n\n    PaymentCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\n            \"succeeded\",\n            \"pending\",\n            \"failed\",\n            \"canceled\",\n            \"requires_action\",\n          ])\n          .default(\"succeeded\"),\n        description: z.string().nullable(),\n        paymentId: z.string().nullable(),\n      }),\n      description: \"Card displaying payment information with amount and status\",\n    },\n\n    SubscriptionCard: {\n      props: z.object({\n        planName: z.string(),\n        status: z\n          .enum([\n            \"active\",\n            \"trialing\",\n            \"past_due\",\n            \"canceled\",\n            \"unpaid\",\n            \"incomplete\",\n          ])\n          .default(\"active\"),\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        interval: z.enum([\"day\", \"week\", \"month\", \"year\"]).default(\"month\"),\n        currentPeriodEnd: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying subscription details with plan, status, and billing info\",\n    },\n\n    InvoiceCard: {\n      props: z.object({\n        invoiceNumber: z.string(),\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\"draft\", \"open\", \"paid\", \"void\", \"uncollectible\"])\n          .default(\"open\"),\n        dueDate: z.string().nullable(),\n        customerEmail: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying invoice details with number, amount, and status\",\n    },\n\n    RefundCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\"pending\", \"succeeded\", \"failed\", \"canceled\"])\n          .default(\"succeeded\"),\n        reason: z.string().nullable(),\n      }),\n      description: \"Card displaying refund information\",\n    },\n\n    DisputeCard: {\n      props: z.object({\n        amount: z.number(),\n        currency: z.string().default(\"usd\"),\n        status: z\n          .enum([\n            \"warning_needs_response\",\n            \"warning_under_review\",\n            \"warning_closed\",\n            \"needs_response\",\n            \"under_review\",\n            \"won\",\n            \"lost\",\n          ])\n          .default(\"needs_response\"),\n        reason: z.string().nullable(),\n        dueDate: z.string().nullable(),\n      }),\n      description:\n        \"Card displaying dispute information with status and deadline\",\n    },\n\n    BalanceCard: {\n      props: z.object({\n        available: z.number(),\n        pending: z.number(),\n        currency: z.string().default(\"usd\"),\n      }),\n      description: \"Card showing account balance breakdown\",\n    },\n\n    // =========================================================================\n    // Chip Components\n    // =========================================================================\n    Chip: {\n      props: z.object({\n        label: z.string(),\n        value: z.string().nullable(),\n        removable: z.boolean().nullable(),\n        action: z.string().nullable(),\n      }),\n      description: \"Chip/tag component for filters or selections\",\n    },\n\n    ChipList: {\n      props: z.object({}),\n      slots: [\"default\"],\n      description: \"Container for multiple chips\",\n    },\n\n    // =========================================================================\n    // Tooltip Component\n    // =========================================================================\n    Tooltip: {\n      props: z.object({\n        content: z.string(),\n        placement: z.enum([\"top\", \"bottom\", \"left\", \"right\"]).default(\"top\"),\n      }),\n      slots: [\"default\"],\n      description: \"Tooltip that appears on hover over child element\",\n    },\n  },\n\n  actions: {\n    // =========================================================================\n    // Customer Actions\n    // =========================================================================\n    fetchCustomers: {\n      params: z.object({\n        limit: z.number().nullable(),\n        email: z.string().nullable(),\n        startingAfter: z.string().nullable(),\n      }),\n      description:\n        \"Fetch customers list with pagination. Data at '/customers/data'\",\n    },\n    viewCustomer: {\n      params: z.object({ customerId: z.string() }),\n      description: \"Open customer details in Stripe Dashboard\",\n    },\n    createCustomer: {\n      params: z.object({\n        email: z.string(),\n        name: z.string().nullable(),\n        phone: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new customer with optional metadata\",\n    },\n    updateCustomer: {\n      params: z.object({\n        customerId: z.string(),\n        email: z.string().nullable(),\n        name: z.string().nullable(),\n        phone: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update an existing customer\",\n    },\n    deleteCustomer: {\n      params: z.object({ customerId: z.string() }),\n      description: \"Delete a customer permanently\",\n    },\n    searchCustomers: {\n      params: z.object({ query: z.string(), limit: z.number().nullable() }),\n      description: \"Search customers by email, name, or metadata\",\n    },\n\n    // =========================================================================\n    // Payment Intent Actions\n    // =========================================================================\n    fetchPayments: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        status: z.string().nullable(),\n        startingAfter: z.string().nullable(),\n      }),\n      description: \"Fetch payment intents list. Data at '/payments/data'\",\n    },\n    viewPayment: {\n      params: z.object({ paymentId: z.string() }),\n      description: \"Open payment details in Stripe Dashboard\",\n    },\n    createPaymentIntent: {\n      params: z.object({\n        amount: z.number(),\n        currency: z.string(),\n        customerId: z.string().nullable(),\n        description: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new payment intent\",\n    },\n    capturePayment: {\n      params: z.object({\n        paymentId: z.string(),\n        amountToCapture: z.number().nullable(),\n      }),\n      description: \"Capture an authorized payment\",\n    },\n    cancelPayment: {\n      params: z.object({\n        paymentId: z.string(),\n        reason: z\n          .enum([\n            \"duplicate\",\n            \"fraudulent\",\n            \"requested_by_customer\",\n            \"abandoned\",\n          ])\n          .nullable(),\n      }),\n      description: \"Cancel a payment intent\",\n    },\n\n    // =========================================================================\n    // Refund Actions\n    // =========================================================================\n    fetchRefunds: {\n      params: z.object({\n        limit: z.number().nullable(),\n        paymentIntentId: z.string().nullable(),\n        chargeId: z.string().nullable(),\n      }),\n      description: \"Fetch refunds list. Data at '/refunds/data'\",\n    },\n    refundPayment: {\n      params: z.object({\n        paymentId: z.string(),\n        amount: z.number().nullable(),\n        reason: z\n          .enum([\"duplicate\", \"fraudulent\", \"requested_by_customer\"])\n          .nullable(),\n      }),\n      description: \"Create a refund for a payment\",\n    },\n    updateRefund: {\n      params: z.object({\n        refundId: z.string(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update refund metadata\",\n    },\n\n    // =========================================================================\n    // Charge Actions\n    // =========================================================================\n    fetchCharges: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        paymentIntentId: z.string().nullable(),\n      }),\n      description: \"Fetch charges list. Data at '/charges/data'\",\n    },\n    captureCharge: {\n      params: z.object({ chargeId: z.string(), amount: z.number().nullable() }),\n      description: \"Capture a previously authorized charge\",\n    },\n\n    // =========================================================================\n    // Subscription Actions\n    // =========================================================================\n    fetchSubscriptions: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\n            \"active\",\n            \"past_due\",\n            \"unpaid\",\n            \"canceled\",\n            \"incomplete\",\n            \"incomplete_expired\",\n            \"trialing\",\n            \"paused\",\n          ])\n          .nullable(),\n        customerId: z.string().nullable(),\n        priceId: z.string().nullable(),\n      }),\n      description: \"Fetch subscriptions. Data at '/subscriptions/data'\",\n    },\n    viewSubscription: {\n      params: z.object({ subscriptionId: z.string() }),\n      description: \"Open subscription details in Stripe Dashboard\",\n    },\n    createSubscription: {\n      params: z.object({\n        customerId: z.string(),\n        priceId: z.string(),\n        quantity: z.number().nullable(),\n        trialPeriodDays: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new subscription for a customer\",\n    },\n    updateSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        priceId: z.string().nullable(),\n        quantity: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a subscription (change price, quantity)\",\n    },\n    cancelSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        immediately: z.boolean().nullable(),\n        cancelAtPeriodEnd: z.boolean().nullable(),\n      }),\n      description: \"Cancel a subscription immediately or at period end\",\n    },\n    pauseSubscription: {\n      params: z.object({\n        subscriptionId: z.string(),\n        resumeAt: z.number().nullable(),\n      }),\n      description: \"Pause a subscription's payment collection\",\n    },\n    resumeSubscription: {\n      params: z.object({ subscriptionId: z.string() }),\n      description: \"Resume a paused subscription\",\n    },\n\n    // =========================================================================\n    // Invoice Actions\n    // =========================================================================\n    fetchInvoices: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\"draft\", \"open\", \"paid\", \"uncollectible\", \"void\"])\n          .nullable(),\n        customerId: z.string().nullable(),\n        subscriptionId: z.string().nullable(),\n      }),\n      description: \"Fetch invoices. Data at '/invoices/data'\",\n    },\n    viewInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Open invoice in Stripe Dashboard\",\n    },\n    createInvoice: {\n      params: z.object({\n        customerId: z.string(),\n        description: z.string().nullable(),\n        daysUntilDue: z.number().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a draft invoice for a customer\",\n    },\n    addInvoiceItem: {\n      params: z.object({\n        invoiceId: z.string(),\n        amount: z.number(),\n        currency: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Add a line item to an invoice\",\n    },\n    finalizeInvoice: {\n      params: z.object({\n        invoiceId: z.string(),\n        autoAdvance: z.boolean().nullable(),\n      }),\n      description: \"Finalize a draft invoice\",\n    },\n    sendInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Send invoice to customer via email\",\n    },\n    payInvoice: {\n      params: z.object({\n        invoiceId: z.string(),\n        paymentMethodId: z.string().nullable(),\n      }),\n      description: \"Pay an open invoice\",\n    },\n    voidInvoice: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Void an invoice (cannot be undone)\",\n    },\n    markInvoiceUncollectible: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Mark invoice as uncollectible\",\n    },\n    downloadInvoicePdf: {\n      params: z.object({ invoiceId: z.string() }),\n      description: \"Download invoice as PDF\",\n    },\n\n    // =========================================================================\n    // Product & Price Actions\n    // =========================================================================\n    fetchProducts: {\n      params: z.object({\n        limit: z.number().nullable(),\n        active: z.boolean().nullable(),\n      }),\n      description: \"Fetch products list. Data at '/products/data'\",\n    },\n    viewProduct: {\n      params: z.object({ productId: z.string() }),\n      description: \"Open product in Stripe Dashboard\",\n    },\n    createProduct: {\n      params: z.object({\n        name: z.string(),\n        description: z.string().nullable(),\n        active: z.boolean().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Create a new product\",\n    },\n    updateProduct: {\n      params: z.object({\n        productId: z.string(),\n        name: z.string().nullable(),\n        description: z.string().nullable(),\n        active: z.boolean().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a product\",\n    },\n    archiveProduct: {\n      params: z.object({ productId: z.string() }),\n      description: \"Archive a product (set active=false)\",\n    },\n    fetchPrices: {\n      params: z.object({\n        limit: z.number().nullable(),\n        productId: z.string().nullable(),\n        active: z.boolean().nullable(),\n        type: z.enum([\"one_time\", \"recurring\"]).nullable(),\n      }),\n      description: \"Fetch prices list. Data at '/prices/data'\",\n    },\n    createPrice: {\n      params: z.object({\n        productId: z.string(),\n        unitAmount: z.number(),\n        currency: z.string(),\n        recurring: z\n          .object({\n            interval: z.enum([\"day\", \"week\", \"month\", \"year\"]),\n            intervalCount: z.number().nullable(),\n          })\n          .nullable(),\n        nickname: z.string().nullable(),\n      }),\n      description: \"Create a new price for a product\",\n    },\n    updatePrice: {\n      params: z.object({\n        priceId: z.string(),\n        active: z.boolean().nullable(),\n        nickname: z.string().nullable(),\n        metadata: z.record(z.string(), z.string()).nullable(),\n      }),\n      description: \"Update a price\",\n    },\n\n    // =========================================================================\n    // Balance & Payout Actions\n    // =========================================================================\n    fetchBalance: {\n      params: z.object({}),\n      description: \"Fetch current account balance. Data at '/balance'\",\n    },\n    fetchPayouts: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z.enum([\"pending\", \"paid\", \"failed\", \"canceled\"]).nullable(),\n      }),\n      description: \"Fetch payouts list. Data at '/payouts/data'\",\n    },\n    createPayout: {\n      params: z.object({\n        amount: z.number(),\n        currency: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Create a payout to your bank account\",\n    },\n    cancelPayout: {\n      params: z.object({ payoutId: z.string() }),\n      description: \"Cancel a pending payout\",\n    },\n\n    // =========================================================================\n    // Dispute Actions\n    // =========================================================================\n    fetchDisputes: {\n      params: z.object({\n        limit: z.number().nullable(),\n        status: z\n          .enum([\n            \"warning_needs_response\",\n            \"warning_under_review\",\n            \"warning_closed\",\n            \"needs_response\",\n            \"under_review\",\n            \"won\",\n            \"lost\",\n          ])\n          .nullable(),\n        chargeId: z.string().nullable(),\n      }),\n      description: \"Fetch disputes list. Data at '/disputes/data'\",\n    },\n    viewDispute: {\n      params: z.object({ disputeId: z.string() }),\n      description: \"Open dispute in Stripe Dashboard\",\n    },\n    updateDispute: {\n      params: z.object({\n        disputeId: z.string(),\n        evidence: z\n          .object({\n            customerName: z.string().nullable(),\n            customerEmailAddress: z.string().nullable(),\n            productDescription: z.string().nullable(),\n            uncategorizedText: z.string().nullable(),\n          })\n          .nullable(),\n        submit: z.boolean().nullable(),\n      }),\n      description: \"Update dispute evidence\",\n    },\n    closeDispute: {\n      params: z.object({ disputeId: z.string() }),\n      description: \"Close a dispute and accept it\",\n    },\n\n    // =========================================================================\n    // Payment Method Actions\n    // =========================================================================\n    fetchPaymentMethods: {\n      params: z.object({\n        customerId: z.string(),\n        type: z\n          .enum([\"card\", \"bank_account\", \"us_bank_account\", \"sepa_debit\"])\n          .nullable(),\n      }),\n      description:\n        \"Fetch customer payment methods. Data at '/paymentMethods/data'\",\n    },\n    attachPaymentMethod: {\n      params: z.object({ paymentMethodId: z.string(), customerId: z.string() }),\n      description: \"Attach a payment method to a customer\",\n    },\n    detachPaymentMethod: {\n      params: z.object({ paymentMethodId: z.string() }),\n      description: \"Detach a payment method from its customer\",\n    },\n    setDefaultPaymentMethod: {\n      params: z.object({ customerId: z.string(), paymentMethodId: z.string() }),\n      description: \"Set the default payment method for a customer\",\n    },\n\n    // =========================================================================\n    // Coupon & Promotion Actions\n    // =========================================================================\n    fetchCoupons: {\n      params: z.object({ limit: z.number().nullable() }),\n      description: \"Fetch coupons list. Data at '/coupons/data'\",\n    },\n    createCoupon: {\n      params: z.object({\n        percentOff: z.number().nullable(),\n        amountOff: z.number().nullable(),\n        currency: z.string().nullable(),\n        duration: z.enum([\"forever\", \"once\", \"repeating\"]),\n        durationInMonths: z.number().nullable(),\n        name: z.string().nullable(),\n        maxRedemptions: z.number().nullable(),\n      }),\n      description: \"Create a new coupon\",\n    },\n    deleteCoupon: {\n      params: z.object({ couponId: z.string() }),\n      description: \"Delete a coupon\",\n    },\n    fetchPromotionCodes: {\n      params: z.object({\n        limit: z.number().nullable(),\n        couponId: z.string().nullable(),\n        active: z.boolean().nullable(),\n      }),\n      description: \"Fetch promotion codes. Data at '/promotionCodes/data'\",\n    },\n    createPromotionCode: {\n      params: z.object({\n        couponId: z.string(),\n        code: z.string().nullable(),\n        maxRedemptions: z.number().nullable(),\n        expiresAt: z.number().nullable(),\n      }),\n      description: \"Create a promotion code for a coupon\",\n    },\n\n    // =========================================================================\n    // Checkout Session Actions\n    // =========================================================================\n    createCheckoutSession: {\n      params: z.object({\n        mode: z.enum([\"payment\", \"subscription\", \"setup\"]),\n        lineItems: z.array(\n          z.object({ priceId: z.string(), quantity: z.number() }),\n        ),\n        successUrl: z.string(),\n        cancelUrl: z.string(),\n        customerId: z.string().nullable(),\n      }),\n      description: \"Create a Checkout Session and get the URL\",\n    },\n    fetchCheckoutSessions: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n        paymentIntentId: z.string().nullable(),\n      }),\n      description: \"Fetch checkout sessions. Data at '/checkoutSessions/data'\",\n    },\n    expireCheckoutSession: {\n      params: z.object({ sessionId: z.string() }),\n      description: \"Expire an open checkout session\",\n    },\n\n    // =========================================================================\n    // Billing Portal Actions\n    // =========================================================================\n    createBillingPortalSession: {\n      params: z.object({ customerId: z.string(), returnUrl: z.string() }),\n      description: \"Create a billing portal session for customer self-service\",\n    },\n\n    // =========================================================================\n    // Webhook & Event Actions\n    // =========================================================================\n    fetchEvents: {\n      params: z.object({\n        limit: z.number().nullable(),\n        type: z.string().nullable(),\n        createdGte: z.number().nullable(),\n        createdLte: z.number().nullable(),\n      }),\n      description: \"Fetch recent events/webhooks. Data at '/events/data'\",\n    },\n\n    // =========================================================================\n    // Setup Intent Actions\n    // =========================================================================\n    createSetupIntent: {\n      params: z.object({\n        customerId: z.string().nullable(),\n        paymentMethodTypes: z.array(z.string()).nullable(),\n        usage: z.enum([\"on_session\", \"off_session\"]).nullable(),\n      }),\n      description: \"Create a SetupIntent for saving payment methods\",\n    },\n    fetchSetupIntents: {\n      params: z.object({\n        limit: z.number().nullable(),\n        customerId: z.string().nullable(),\n      }),\n      description: \"Fetch setup intents. Data at '/setupIntents/data'\",\n    },\n    cancelSetupIntent: {\n      params: z.object({ setupIntentId: z.string() }),\n      description: \"Cancel a setup intent\",\n    },\n\n    // =========================================================================\n    // Tax Rate Actions\n    // =========================================================================\n    fetchTaxRates: {\n      params: z.object({\n        limit: z.number().nullable(),\n        active: z.boolean().nullable(),\n        inclusive: z.boolean().nullable(),\n      }),\n      description: \"Fetch tax rates. Data at '/taxRates/data'\",\n    },\n    createTaxRate: {\n      params: z.object({\n        displayName: z.string(),\n        percentage: z.number(),\n        inclusive: z.boolean(),\n        jurisdiction: z.string().nullable(),\n        description: z.string().nullable(),\n      }),\n      description: \"Create a new tax rate\",\n    },\n\n    // =========================================================================\n    // Data & Refresh Actions\n    // =========================================================================\n    refreshData: {\n      params: z.object({}),\n      description: \"Refresh all dashboard data\",\n    },\n    refreshCustomers: {\n      params: z.object({}),\n      description: \"Refresh customers data only\",\n    },\n    refreshPayments: {\n      params: z.object({}),\n      description: \"Refresh payments data only\",\n    },\n    refreshSubscriptions: {\n      params: z.object({}),\n      description: \"Refresh subscriptions data only\",\n    },\n    refreshInvoices: {\n      params: z.object({}),\n      description: \"Refresh invoices data only\",\n    },\n    exportData: {\n      params: z.object({\n        format: z.enum([\"csv\", \"json\"]),\n        dataType: z.enum([\n          \"customers\",\n          \"payments\",\n          \"subscriptions\",\n          \"invoices\",\n        ]),\n      }),\n      description: \"Export data as CSV or JSON\",\n    },\n\n    // =========================================================================\n    // Navigation Actions\n    // =========================================================================\n    navigate: {\n      params: z.object({ path: z.string() }),\n      description: \"Navigate to a Dashboard page\",\n    },\n    openDashboard: {\n      params: z.object({\n        page: z\n          .enum([\n            \"home\",\n            \"payments\",\n            \"customers\",\n            \"products\",\n            \"subscriptions\",\n            \"invoices\",\n            \"connect\",\n            \"reports\",\n            \"developers\",\n          ])\n          .nullable(),\n      }),\n      description: \"Open Stripe Dashboard page in new tab\",\n    },\n    openExternalLink: {\n      params: z.object({ url: z.string() }),\n      description: \"Open an external URL in new tab\",\n    },\n\n    // =========================================================================\n    // Form Actions\n    // =========================================================================\n    submitForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Submit form data\",\n    },\n    resetForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Reset form to initial values\",\n    },\n    validateForm: {\n      params: z.object({ formId: z.string().nullable() }),\n      description: \"Validate form without submitting\",\n    },\n    setFormValue: {\n      params: z.object({ path: z.string(), value: z.unknown() }),\n      description: \"Set a specific form field value\",\n    },\n\n    // =========================================================================\n    // UI Actions\n    // =========================================================================\n    showToast: {\n      params: z.object({\n        message: z.string(),\n        type: z.enum([\"success\", \"error\", \"warning\", \"info\"]).nullable(),\n      }),\n      description: \"Show a toast notification\",\n    },\n    copyToClipboard: {\n      params: z.object({ text: z.string() }),\n      description: \"Copy text to clipboard\",\n    },\n    setLoading: {\n      params: z.object({\n        loading: z.boolean(),\n        message: z.string().nullable(),\n      }),\n      description: \"Show/hide loading state\",\n    },\n\n    // =========================================================================\n    // Filter & Sort Actions\n    // =========================================================================\n    setFilter: {\n      params: z.object({ key: z.string(), value: z.unknown() }),\n      description: \"Set a filter value\",\n    },\n    clearFilters: {\n      params: z.object({}),\n      description: \"Clear all filters\",\n    },\n    setSort: {\n      params: z.object({\n        field: z.string(),\n        direction: z.enum([\"asc\", \"desc\"]),\n      }),\n      description: \"Set sort field and direction\",\n    },\n    setPageSize: {\n      params: z.object({ size: z.number() }),\n      description: \"Set items per page\",\n    },\n    goToPage: {\n      params: z.object({ page: z.number() }),\n      description: \"Navigate to a specific page\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/render/index.ts",
    "content": "// Catalog\nexport { stripeCatalog } from \"./catalog\";\n\n// Components\nexport { components, Fallback } from \"./catalog/components\";\n\n// Actions\nexport { actionHandlers, executeAction } from \"./catalog/actions\";\n\n// Renderer\nexport { StripeRenderer, type StripeRendererProps } from \"./renderer\";\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/render/renderer.tsx",
    "content": "import { useMemo, useRef, type ReactNode } from \"react\";\nimport {\n  Renderer,\n  type ComponentRegistry,\n  type Spec,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n} from \"@json-render/react\";\n\nimport { components, Fallback } from \"./catalog/components\";\nimport { executeAction } from \"./catalog/actions\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\nexport interface StripeRendererProps {\n  /** The UI spec to render (from json-render) */\n  spec: Spec | null;\n  /** Data context for components */\n  data?: Record<string, unknown>;\n  /** Function to update data */\n  setData?: SetState;\n  /** Callback when data changes */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n}\n\n// =============================================================================\n// Build Registry\n// =============================================================================\n\n/**\n * Build a component registry from our components map.\n * Uses refs to avoid recreating on data changes.\n */\nfunction buildRegistry(\n  dataRef: React.RefObject<Record<string, unknown>>,\n  setDataRef: React.RefObject<SetState | undefined>,\n  loading?: boolean,\n): ComponentRegistry {\n  const registry: ComponentRegistry = {};\n\n  for (const [name, Component] of Object.entries(components)) {\n    const noop = () => {};\n    registry[name] = (renderProps: {\n      element: { type: string; props: Record<string, unknown> };\n      children?: ReactNode;\n      emit?: (event: string) => void;\n    }) =>\n      Component({\n        element: renderProps.element,\n        children: renderProps.children,\n        emit: renderProps.emit ?? noop,\n        loading,\n        state: dataRef.current,\n        getValue: (path: string) => {\n          const data = dataRef.current;\n          const parts = path.replace(/^\\//, \"\").split(\"/\");\n          let current: unknown = data;\n          for (const part of parts) {\n            if (current && typeof current === \"object\" && part in current) {\n              current = (current as Record<string, unknown>)[part];\n            } else {\n              return undefined;\n            }\n          }\n          return current;\n        },\n        onAction: (actionName: string, params?: Record<string, unknown>) => {\n          const setState = setDataRef.current;\n          if (setState) {\n            executeAction(actionName, params, setState, dataRef.current);\n          }\n        },\n      });\n  }\n\n  return registry;\n}\n\n/**\n * Fallback component for unknown types\n */\nconst fallbackRegistry = (renderProps: {\n  element: { type: string; props: Record<string, unknown> };\n}) => <Fallback element={renderProps.element} />;\n\n// =============================================================================\n// StripeRenderer Component\n// =============================================================================\n\n/**\n * Main renderer component for Stripe UIXT.\n *\n * Wraps the json-render Renderer with all necessary providers\n * and connects it to the Stripe component implementations.\n */\nexport function StripeRenderer({\n  spec,\n  data = {},\n  setData,\n  onStateChange,\n  loading,\n}: StripeRendererProps): ReactNode {\n  // Use refs to keep registry stable while still accessing latest data/setData\n  const dataRef = useRef(data);\n  const setDataRef = useRef(setData);\n  dataRef.current = data;\n  setDataRef.current = setData;\n\n  // Memoize registry - only changes when loading changes\n  const registry = useMemo(\n    () => buildRegistry(dataRef, setDataRef, loading),\n    [loading],\n  );\n\n  const mergedState = useMemo(\n    () => (spec?.state ? { ...data, ...spec.state } : data),\n    [data, spec?.state],\n  );\n\n  if (!spec) return null;\n\n  return (\n    <StateProvider initialState={mergedState} onStateChange={onStateChange}>\n      <VisibilityProvider>\n        <ActionProvider>\n          <Renderer\n            spec={spec}\n            registry={registry}\n            fallback={fallbackRegistry}\n            loading={loading}\n          />\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/stream-spec.ts",
    "content": "import type { Spec } from \"@json-render/react\";\n\ninterface JsonPatch {\n  op: string;\n  path: string;\n  value?: unknown;\n  from?: string;\n}\n\nfunction setDeep(\n  obj: Record<string, unknown>,\n  segments: string[],\n  value: unknown,\n): void {\n  let current: unknown = obj;\n  for (let i = 0; i < segments.length - 1; i++) {\n    const seg = segments[i];\n    const next = (current as Record<string, unknown>)[seg];\n    if (next && typeof next === \"object\") {\n      current = next;\n    } else {\n      const container = /^\\d+$/.test(segments[i + 1]) ? [] : {};\n      (current as Record<string, unknown>)[seg] = container;\n      current = container;\n    }\n  }\n  const last = segments[segments.length - 1];\n  if (Array.isArray(current)) {\n    (current as unknown[])[Number(last)] = value;\n  } else {\n    (current as Record<string, unknown>)[last] = value;\n  }\n}\n\nfunction setSpecValue(spec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    (spec as Record<string, unknown>).root = value as string;\n    return;\n  }\n  if (path === \"/state\") {\n    (spec as Record<string, unknown>).state = value;\n    return;\n  }\n  if (path.startsWith(\"/state/\")) {\n    if (!spec.state) (spec as Record<string, unknown>).state = {};\n    const segments = path.slice(\"/state/\".length).split(\"/\");\n    setDeep(spec.state as Record<string, unknown>, segments, value);\n    return;\n  }\n  const elemMatch = path.match(/^\\/elements\\/(.+)/);\n  if (elemMatch) {\n    spec.elements[elemMatch[1]] = value as Spec[\"elements\"][string];\n  }\n}\n\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const next: Spec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\":\n      setSpecValue(next, patch.path, patch.value);\n      break;\n    case \"remove\":\n      if (patch.path.startsWith(\"/elements/\")) {\n        const key = patch.path.slice(\"/elements/\".length);\n        delete next.elements[key];\n      }\n      break;\n  }\n  return next;\n}\n\n/**\n * Fetch an AI generation endpoint that returns a JSONL patch stream,\n * applying patches progressively via onPatch.\n */\nexport async function streamSpec(\n  url: string,\n  body: Record<string, unknown>,\n  onPatch: (spec: Spec) => void,\n): Promise<Spec> {\n  const response = await fetch(url, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(body),\n  });\n\n  if (!response.ok) {\n    let msg = `API error: ${response.status}`;\n    try {\n      const errData = await response.json();\n      if (errData.error) msg = errData.error;\n    } catch {\n      // use default\n    }\n    throw new Error(msg);\n  }\n\n  const reader = response.body?.getReader();\n  if (!reader) throw new Error(\"No response body\");\n\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n  let spec: Spec = { root: \"\", elements: {} };\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n\n    buffer += decoder.decode(value, { stream: true });\n    const lines = buffer.split(\"\\n\");\n    buffer = lines.pop() ?? \"\";\n\n    for (const line of lines) {\n      const trimmed = line.trim();\n      if (!trimmed || trimmed.startsWith(\"//\")) continue;\n      try {\n        const parsed = JSON.parse(trimmed) as JsonPatch;\n        if (parsed.op) {\n          spec = applyPatch(spec, parsed);\n          onPatch(spec);\n        }\n      } catch {\n        // skip non-JSON lines\n      }\n    }\n  }\n\n  if (buffer.trim()) {\n    try {\n      const parsed = JSON.parse(buffer.trim()) as JsonPatch;\n      if (parsed.op) {\n        spec = applyPatch(spec, parsed);\n        onPatch(spec);\n      }\n    } catch {\n      // skip\n    }\n  }\n\n  return spec;\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/lib/stripe.ts",
    "content": "import Stripe from \"stripe\";\nimport {\n  createHttpClient,\n  STRIPE_API_KEY,\n} from \"@stripe/ui-extension-sdk/http_client\";\n\n/**\n * Create a Stripe client for use in Stripe Apps.\n *\n * The Stripe UI Extension SDK handles authentication automatically\n * through the httpClient - no real API key is needed.\n */\nexport const stripe = new Stripe(STRIPE_API_KEY, {\n  httpClient: createHttpClient(),\n  apiVersion: \"2024-12-18.acacia\",\n});\n\n/**\n * Format amount for display (converts cents to dollars)\n */\nexport function formatAmount(amount: number, currency = \"usd\"): string {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency: currency.toUpperCase(),\n  }).format(amount / 100);\n}\n\n/**\n * Format date for display\n */\nexport function formatDate(timestamp: number): string {\n  return new Date(timestamp * 1000).toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  });\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/src/views/FullPage.tsx",
    "content": "import { useState, useCallback, useEffect } from \"react\";\nimport type { Spec } from \"@json-render/react\";\nimport {\n  Box,\n  FullPageView,\n  FullPageTabs,\n  FullPageTab,\n  Button,\n  TextField,\n  Divider,\n  List,\n  ListItem,\n  Spinner,\n} from \"@stripe/ui-extension-sdk/ui\";\nimport type { ExtensionContextValue } from \"@stripe/ui-extension-sdk/context\";\n\nimport { stripeCatalog, StripeRenderer } from \"../lib/render\";\nimport { executeAction } from \"../lib/render/catalog/actions\";\nimport { API_GENERATE_URL } from \"../lib/config\";\nimport { streamSpec } from \"../lib/stream-spec\";\n\nfunction createOverviewSpec(data: Record<string, unknown>): Spec {\n  const customers = data.customers as { total?: number } | undefined;\n  const payments = data.payments as\n    | { totalVolume?: string; successRate?: string }\n    | undefined;\n  const subscriptions = data.subscriptions as\n    | { active?: number; trialing?: number; pastDue?: number }\n    | undefined;\n\n  return {\n    root: \"root\",\n    elements: {\n      root: {\n        type: \"Stack\",\n        props: { direction: \"vertical\", gap: \"large\" },\n        children: [\"metrics\", \"divider\", \"refresh\"],\n      },\n      metrics: {\n        type: \"Stack\",\n        props: { direction: \"horizontal\", gap: \"medium\" },\n        children: [\"m1\", \"m2\", \"m3\"],\n      },\n      m1: {\n        type: \"Metric\",\n        props: {\n          label: \"Payment Volume\",\n          value: payments?.totalVolume ?? \"$0\",\n          change: payments?.successRate\n            ? `${payments.successRate} success`\n            : null,\n          changeType: \"positive\",\n        },\n        children: [],\n      },\n      m2: {\n        type: \"Metric\",\n        props: {\n          label: \"Active Subscriptions\",\n          value: String(subscriptions?.active ?? 0),\n          change: subscriptions?.trialing\n            ? `+${subscriptions.trialing} trialing`\n            : null,\n          changeType: \"positive\",\n        },\n        children: [],\n      },\n      m3: {\n        type: \"Metric\",\n        props: {\n          label: \"Total Customers\",\n          value: String(customers?.total ?? 0),\n          changeType: \"neutral\",\n        },\n        children: [],\n      },\n      divider: { type: \"Divider\", props: {}, children: [] },\n      refresh: {\n        type: \"Button\",\n        props: {\n          label: \"Refresh Data\",\n          action: \"refreshData\",\n          type: \"secondary\",\n        },\n        children: [],\n      },\n    },\n  };\n}\n\nconst componentCatalogInfo = Object.entries(stripeCatalog.data.components).map(\n  ([name, def]) => ({\n    name,\n    description: def.description,\n  }),\n);\n\nconst actionsCatalogInfo = Object.entries(stripeCatalog.data.actions).map(\n  ([name, def]) => ({\n    name,\n    description: def.description,\n  }),\n);\n\nconst FullPage = (_props: ExtensionContextValue) => {\n  const [prompt, setPrompt] = useState(\"\");\n  const [isGenerating, setIsGenerating] = useState(false);\n  const [isLoading, setIsLoading] = useState(true);\n  const [currentSpec, setCurrentSpec] = useState<Spec | null>(null);\n  const [data, setState] = useState<Record<string, unknown>>({});\n  const [error, setError] = useState<string | null>(null);\n\n  const handleSetState = useCallback(\n    (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {\n      setState((prev) => updater(prev));\n    },\n    [],\n  );\n\n  useEffect(() => {\n    async function loadData() {\n      setIsLoading(true);\n      try {\n        await executeAction(\"refreshData\", {}, handleSetState, {});\n      } catch (err) {\n        console.error(\"Failed to load data:\", err);\n        setError(\"Failed to load Stripe data\");\n      } finally {\n        setIsLoading(false);\n      }\n    }\n    loadData();\n  }, [handleSetState]);\n\n  const handleGenerate = async () => {\n    if (!prompt.trim()) return;\n    setIsGenerating(true);\n    setError(null);\n\n    try {\n      await streamSpec(\n        API_GENERATE_URL,\n        {\n          prompt,\n          systemPrompt: stripeCatalog.prompt({\n            system:\n              \"You are a Stripe dashboard builder. Generate UI specs for displaying Stripe data in a full-page layout.\",\n          }),\n        },\n        (spec) => setCurrentSpec(spec),\n      );\n    } catch (err) {\n      setCurrentSpec(createOverviewSpec(data));\n      setError(\n        `Using local generation (${err instanceof Error ? err.message : \"API unavailable\"})`,\n      );\n    } finally {\n      setIsGenerating(false);\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <FullPageView>\n        <FullPageTabs>\n          <FullPageTab\n            id=\"overview\"\n            label=\"Overview\"\n            content={\n              <Box\n                css={{\n                  stack: \"y\",\n                  gap: \"medium\",\n                  alignX: \"center\",\n                  paddingY: \"xlarge\",\n                }}\n              >\n                <Spinner size=\"large\" />\n                <Box css={{ font: \"body\", color: \"secondary\" }}>\n                  Loading Stripe data...\n                </Box>\n              </Box>\n            }\n          />\n        </FullPageTabs>\n      </FullPageView>\n    );\n  }\n\n  return (\n    <FullPageView\n      pageAction={{\n        label: \"Refresh Data\",\n        onPress: () => executeAction(\"refreshData\", {}, handleSetState, {}),\n      }}\n    >\n      <FullPageTabs>\n        <FullPageTab\n          id=\"overview\"\n          label=\"Overview\"\n          content={\n            <Box css={{ stack: \"y\", gap: \"large\", padding: \"large\" }}>\n              <StripeRenderer\n                spec={createOverviewSpec(data)}\n                data={data}\n                setData={handleSetState}\n                loading={false}\n              />\n            </Box>\n          }\n        />\n\n        <FullPageTab\n          id=\"generate\"\n          label=\"Generate\"\n          content={\n            <Box css={{ stack: \"y\", gap: \"medium\", padding: \"large\" }}>\n              <TextField\n                label=\"Describe the dashboard you want\"\n                placeholder=\"e.g., Show MRR, payments, or subscriptions\"\n                value={prompt}\n                onChange={(e) => setPrompt(e.target.value)}\n              />\n              <Button\n                type=\"primary\"\n                onPress={handleGenerate}\n                disabled={isGenerating || !prompt.trim()}\n              >\n                {isGenerating ? \"Generating...\" : \"Generate\"}\n              </Button>\n\n              {error && (\n                <Box css={{ color: \"secondary\", font: \"caption\" }}>{error}</Box>\n              )}\n\n              {currentSpec && (\n                <>\n                  <Divider />\n                  <Box css={{ font: \"subheading\" }}>\n                    Preview (with real Stripe data)\n                  </Box>\n                  <Box\n                    css={{\n                      padding: \"medium\",\n                      borderRadius: \"medium\",\n                      keyline: \"neutral\",\n                    }}\n                  >\n                    <StripeRenderer\n                      spec={currentSpec}\n                      data={data}\n                      setData={handleSetState}\n                      loading={isGenerating}\n                    />\n                  </Box>\n                </>\n              )}\n            </Box>\n          }\n        />\n\n        <FullPageTab\n          id=\"components\"\n          label=\"Components\"\n          content={\n            <Box css={{ padding: \"large\" }}>\n              <Box css={{ font: \"subheading\", marginBottom: \"medium\" }}>\n                Available Components ({componentCatalogInfo.length})\n              </Box>\n              <List>\n                {componentCatalogInfo.map((comp) => (\n                  <ListItem\n                    key={comp.name}\n                    title={comp.name}\n                    secondaryTitle={comp.description}\n                  />\n                ))}\n              </List>\n            </Box>\n          }\n        />\n\n        <FullPageTab\n          id=\"actions\"\n          label=\"Actions\"\n          content={\n            <Box css={{ padding: \"large\" }}>\n              <Box css={{ font: \"subheading\", marginBottom: \"medium\" }}>\n                Available Actions ({actionsCatalogInfo.length})\n              </Box>\n              <List>\n                {actionsCatalogInfo.map((action) => (\n                  <ListItem\n                    key={action.name}\n                    title={action.name}\n                    secondaryTitle={action.description}\n                  />\n                ))}\n              </List>\n            </Box>\n          }\n        />\n      </FullPageTabs>\n    </FullPageView>\n  );\n};\n\nexport default FullPage;\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/stripe-app.template.json",
    "content": "{\n    \"id\": \"com.example.json-render-demo\",\n    \"version\": \"0.0.1\",\n    \"name\": \"json-render Full Page Demo\",\n    \"icon\": \"\",\n    \"permissions\": [\n        {\n            \"permission\": \"customer_read\",\n            \"purpose\": \"Display customer information in dashboards\"\n        },\n        {\n            \"permission\": \"customer_write\",\n            \"purpose\": \"Create and manage customers\"\n        },\n        {\n            \"permission\": \"payment_intent_read\",\n            \"purpose\": \"Display payment information in dashboards\"\n        },\n        {\n            \"permission\": \"charge_read\",\n            \"purpose\": \"Display charge information and details\"\n        },\n        {\n            \"permission\": \"subscription_read\",\n            \"purpose\": \"Display subscription information in dashboards\"\n        },\n        {\n            \"permission\": \"subscription_write\",\n            \"purpose\": \"Manage subscriptions\"\n        },\n        {\n            \"permission\": \"invoice_read\",\n            \"purpose\": \"Display invoice information in dashboards\"\n        },\n        {\n            \"permission\": \"invoice_write\",\n            \"purpose\": \"Send and manage invoices\"\n        },\n        {\n            \"permission\": \"charge_write\",\n            \"purpose\": \"Process charges and refunds\"\n        },\n        {\n            \"permission\": \"product_read\",\n            \"purpose\": \"Display product catalog\"\n        },\n        {\n            \"permission\": \"product_write\",\n            \"purpose\": \"Create and manage products\"\n        },\n        {\n            \"permission\": \"plan_read\",\n            \"purpose\": \"Display pricing information\"\n        },\n        {\n            \"permission\": \"plan_write\",\n            \"purpose\": \"Create and manage prices\"\n        },\n        {\n            \"permission\": \"balance_read\",\n            \"purpose\": \"Display account balance\"\n        },\n        {\n            \"permission\": \"payout_read\",\n            \"purpose\": \"Display payout information\"\n        },\n        {\n            \"permission\": \"event_read\",\n            \"purpose\": \"Display webhook events\"\n        },\n        {\n            \"permission\": \"dispute_read\",\n            \"purpose\": \"Display dispute information\"\n        },\n        {\n            \"permission\": \"coupon_read\",\n            \"purpose\": \"Display coupon information\"\n        },\n        {\n            \"permission\": \"coupon_write\",\n            \"purpose\": \"Create and manage coupons\"\n        }\n    ],\n    \"ui_extension\": {\n        \"views\": [\n            {\n                \"viewport\": \"stripe.dashboard.fullpage\",\n                \"component\": \"FullPage\"\n            }\n        ],\n        \"content_security_policy\": {\n            \"connect-src\": null,\n            \"image-src\": null,\n            \"purpose\": \"No external connections needed for core functionality\"\n        }\n    }\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/tsconfig.json",
    "content": "{\n  \"extends\": \"@stripe/ui-extension-tools/tsconfig.ui-extension\"\n}\n"
  },
  {
    "path": "examples/stripe-app/fullpage-app/ui-extensions.d.ts",
    "content": "/// <reference types=\"@stripe/ui-extension-tools\" />\n"
  },
  {
    "path": "examples/svelte/CHANGELOG.md",
    "content": "# example-svelte\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/svelte@0.14.1\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/svelte@0.14.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/svelte@0.13.0\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/svelte@0.12.1\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/svelte@0.12.0\n"
  },
  {
    "path": "examples/svelte/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>json-render svelte example</title>\n    <meta property=\"og:title\" content=\"Svelte Example | json-render\" />\n    <meta property=\"og:image\" content=\"/og-image.png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"Svelte Example | json-render\" />\n    <meta name=\"twitter:image\" content=\"/og-image.png\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/svelte/package.json",
    "content": "{\n  \"name\": \"example-svelte\",\n  \"version\": \"0.1.6\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"portless svelte.json-render vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"check-types\": \"svelte-check --tsconfig ./tsconfig.json\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/svelte\": \"workspace:*\",\n    \"svelte\": \"^5.49.2\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n    \"svelte-check\": \"^4.3.6\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\"\n  }\n}\n"
  },
  {
    "path": "examples/svelte/src/App.svelte",
    "content": "<script lang=\"ts\">\n  import { StateProvider } from \"@json-render/svelte\";\n  import { demoSpec } from \"./lib/spec\";\n  import DemoRenderer from \"./DemoRenderer.svelte\";\n\n  const initialState = demoSpec.state ?? {};\n</script>\n\n<StateProvider {initialState}>\n  <DemoRenderer />\n</StateProvider>\n"
  },
  {
    "path": "examples/svelte/src/DemoRenderer.svelte",
    "content": "<script lang=\"ts\">\n  import {\n    ActionProvider,\n    ValidationProvider,\n    VisibilityProvider,\n    Renderer,\n    getStateContext,\n  } from \"@json-render/svelte\";\n  import { demoSpec } from \"./lib/spec\";\n  import { registry } from \"./lib/registry\";\n\n  const state = getStateContext();\n\n  const handlers = {\n    increment: async () => {\n      state.set(\"/count\", Number(state.get(\"/count\") || 0) + 1);\n    },\n    decrement: async () => {\n      state.set(\"/count\", Math.max(0, Number(state.get(\"/count\") || 0) - 1));\n    },\n    reset: async () => {\n      state.set(\"/count\", 0);\n    },\n    toggleItem: async (params: Record<string, unknown>) => {\n      const index = params.index as number;\n      const todos = (\n        state.get(\"/todos\") as Array<{\n          id: number;\n          title: string;\n          completed: boolean;\n        }>\n      ).map((item, i) =>\n        i === index ? { ...item, completed: !item.completed } : item,\n      );\n      state.set(\"/todos\", todos);\n    },\n  };\n</script>\n\n<ActionProvider {handlers}>\n  <VisibilityProvider>\n    <ValidationProvider>\n      <Renderer spec={demoSpec} {registry} />\n    </ValidationProvider>\n  </VisibilityProvider>\n</ActionProvider>\n"
  },
  {
    "path": "examples/svelte/src/app.css",
    "content": "* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nbody {\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n  background: #f9fafb;\n  color: #111827;\n  min-height: 100vh;\n}\n"
  },
  {
    "path": "examples/svelte/src/lib/catalog.ts",
    "content": "import { schema } from \"@json-render/svelte/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = schema.createCatalog({\n  components: {\n    Stack: {\n      props: z.object({\n        gap: z.number().optional(),\n        padding: z.number().optional(),\n        direction: z.enum([\"vertical\", \"horizontal\"]).optional(),\n        align: z.enum([\"start\", \"center\", \"end\"]).optional(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Layout container that stacks children vertically or horizontally\",\n    },\n    Card: {\n      props: z.object({\n        title: z.string().optional(),\n        subtitle: z.string().optional(),\n      }),\n      slots: [\"default\"],\n      description: \"A card container with optional title and subtitle\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).optional(),\n        weight: z.enum([\"normal\", \"medium\", \"bold\"]).optional(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"Displays a text string\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).optional(),\n        disabled: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A clickable button that emits a 'press' event\",\n    },\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A small badge/tag label\",\n    },\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().optional(),\n        completed: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A single item in a list\",\n    },\n    Input: {\n      props: z.object({\n        value: z.string().optional(),\n        placeholder: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A text input field that supports two-way state binding\",\n    },\n  },\n  actions: {\n    increment: {\n      params: z.object({}),\n      description: \"Increment the counter by 1\",\n    },\n    decrement: {\n      params: z.object({}),\n      description: \"Decrement the counter by 1\",\n    },\n    reset: {\n      params: z.object({}),\n      description: \"Reset the counter to 0\",\n    },\n    toggleItem: {\n      params: z.object({\n        index: z.number(),\n      }),\n      description: \"Toggle the completed state of a todo item\",\n    },\n  },\n});\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Badge.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    color?: string;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<span\n  style=\"\n    display: inline-block;\n    padding: 4px 12px;\n    border-radius: 999px;\n    font-size: 13px;\n    font-weight: 500;\n    background-color: {props.color ? `${props.color}20` : '#e0f2fe'};\n    color: {props.color ?? '#0369a1'};\n    border: 1px solid {props.color ? `${props.color}40` : '#bae6fd'};\n  \">\n  {props.label}\n</span>\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Button.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    variant?: \"primary\" | \"secondary\" | \"danger\";\n    disabled?: boolean;\n  }> {}\n\n  let { props, emit }: Props = $props();\n\n  let backgroundColor = $derived(\n    props.variant === \"danger\"\n      ? \"#fee2e2\"\n      : props.variant === \"secondary\"\n        ? \"#f3f4f6\"\n        : \"#3b82f6\",\n  );\n  let textColor = $derived(\n    props.variant === \"danger\"\n      ? \"#dc2626\"\n      : props.variant === \"secondary\"\n        ? \"#374151\"\n        : \"white\",\n  );\n</script>\n\n<button\n  disabled={props.disabled}\n  onclick={() => emit(\"press\")}\n  style=\"\n    padding: 8px 16px;\n    border-radius: 8px;\n    border: none;\n    cursor: {props.disabled ? 'not-allowed' : 'pointer'};\n    font-weight: 500;\n    font-size: 14px;\n    transition: background 0.15s;\n    opacity: {props.disabled ? '0.5' : '1'};\n    background-color: {backgroundColor};\n    color: {textColor};\n  \">\n  {props.label}\n</button>\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Card.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string;\n    subtitle?: string;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n</script>\n\n<div\n  style=\"\n    background: white;\n    border-radius: 12px;\n    border: 1px solid #e5e7eb;\n    padding: 20px;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.05);\n  \">\n  {#if props.title}\n    <h2\n      style=\"\n        font-size: 16px;\n        font-weight: 600;\n        color: #111827;\n        margin: 0 0 4px 0;\n      \">\n      {props.title}\n    </h2>\n  {/if}\n  {#if props.subtitle}\n    <p\n      style=\"\n        font-size: 13px;\n        color: #6b7280;\n        margin: 0 0 12px 0;\n      \">\n      {props.subtitle}\n    </p>\n  {/if}\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Input.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { getBoundProp } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    value?: string;\n    placeholder?: string;\n  }> {}\n\n  let { props, bindings }: Props = $props();\n\n  let value = getBoundProp<string>(\n    () => props.value,\n    () => bindings?.value,\n  );\n</script>\n\n<input\n  bind:value={value.current}\n  placeholder={props.placeholder ?? \"\"}\n  style=\"\n    padding: 8px 12px;\n    border-radius: 8px;\n    border: 1px solid #d1d5db;\n    font-size: 14px;\n    outline: none;\n    width: 100%;\n    box-sizing: border-box;\n  \" />\n"
  },
  {
    "path": "examples/svelte/src/lib/components/ListItem.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title: string;\n    completed?: boolean;\n  }> {}\n\n  let { props, emit }: Props = $props();\n</script>\n\n<button\n  type=\"button\"\n  onclick={() => emit(\"press\")}\n  style=\"\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    padding: 10px 12px;\n    border-radius: 8px;\n    cursor: pointer;\n    background-color: {props.completed ? '#f0fdf4' : '#f9fafb'};\n    border: 1px solid {props.completed ? '#bbf7d0' : '#e5e7eb'};\n  \">\n  <div\n    style=\"\n      width: 18px;\n      height: 18px;\n      border-radius: 50%;\n      border: 2px solid {props.completed ? '#16a34a' : '#d1d5db'};\n      background-color: {props.completed ? '#16a34a' : 'transparent'};\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      color: white;\n      font-size: 11px;\n      flex-shrink: 0;\n    \">\n    {props.completed ? \"✓\" : \"\"}\n  </div>\n  <span\n    style=\"\n      font-size: 14px;\n      color: {props.completed ? '#6b7280' : '#111827'};\n      text-decoration: {props.completed ? 'line-through' : 'none'};\n    \">\n    {props.title}\n  </span>\n</button>\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Stack.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    gap?: number;\n    padding?: number;\n    direction?: \"vertical\" | \"horizontal\";\n    align?: \"start\" | \"center\" | \"end\";\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n  let horizontal = $derived(props.direction === \"horizontal\");\n</script>\n\n<div\n  style=\"\n    display: flex;\n    flex-direction: {horizontal ? 'row' : 'column'};\n    gap: {props.gap ?? 0}px;\n    padding: {props.padding ?? 0}px;\n    align-items: {props.align === 'start'\n    ? 'flex-start'\n    : props.align === 'end'\n      ? 'flex-end'\n      : horizontal\n        ? 'center'\n        : 'stretch'};\n  \">\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte/src/lib/components/Text.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    content: string;\n    size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n    weight?: \"normal\" | \"medium\" | \"bold\";\n    color?: string;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const sizeMap: Record<string, string> = {\n    sm: \"12px\",\n    md: \"14px\",\n    lg: \"16px\",\n    xl: \"24px\",\n  };\n\n  const weightMap: Record<string, string> = {\n    normal: \"400\",\n    medium: \"500\",\n    bold: \"700\",\n  };\n</script>\n\n<span\n  style=\"\n    font-size: {sizeMap[props.size ?? 'md'] ?? '14px'};\n    font-weight: {weightMap[props.weight ?? 'normal'] ?? '400'};\n    color: {props.color ?? '#111827'};\n  \">\n  {String(props.content ?? \"\")}\n</span>\n"
  },
  {
    "path": "examples/svelte/src/lib/registry.ts",
    "content": "import { type Components, defineRegistry } from \"@json-render/svelte\";\nimport { catalog } from \"./catalog\";\n\nimport Stack from \"./components/Stack.svelte\";\nimport Card from \"./components/Card.svelte\";\nimport Text from \"./components/Text.svelte\";\nimport Button from \"./components/Button.svelte\";\nimport Badge from \"./components/Badge.svelte\";\nimport ListItem from \"./components/ListItem.svelte\";\nimport Input from \"./components/Input.svelte\";\n\nconst components: Components<typeof catalog> = {\n  Stack,\n  Card,\n  Text,\n  Button,\n  Badge,\n  ListItem,\n  Input,\n};\n\nexport const { registry } = defineRegistry(catalog, { components });\n"
  },
  {
    "path": "examples/svelte/src/lib/spec.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport const demoSpec: Spec = {\n  root: \"root\",\n  state: {\n    count: 0,\n    name: \"\",\n    todos: [\n      { id: 1, title: \"Learn Svelte 5\", completed: true },\n      { id: 2, title: \"Try @json-render/svelte\", completed: false },\n      { id: 3, title: \"Build something awesome\", completed: false },\n    ],\n  },\n  elements: {\n    root: {\n      type: \"Stack\",\n      props: { gap: 24, padding: 24, direction: \"vertical\" },\n      children: [\n        \"header\",\n        \"counter-card\",\n        \"milestone-badge\",\n        \"todos-card\",\n        \"input-card\",\n      ],\n    },\n    header: {\n      type: \"Text\",\n      props: {\n        content: \"@json-render/svelte demo\",\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"counter-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Counter\",\n        subtitle: \"Click the buttons to change the count\",\n      },\n      children: [\"counter-body\"],\n    },\n    \"counter-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"horizontal\", align: \"center\" },\n      children: [\n        \"decrement-btn\",\n        \"counter-value\",\n        \"increment-btn\",\n        \"reset-btn\",\n      ],\n    },\n    \"decrement-btn\": {\n      type: \"Button\",\n      props: { label: \"−\", variant: \"secondary\" },\n      on: { press: { action: \"decrement\" } },\n    },\n    \"counter-value\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/count\" },\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"increment-btn\": {\n      type: \"Button\",\n      props: { label: \"+\", variant: \"primary\" },\n      on: { press: { action: \"increment\" } },\n    },\n    \"reset-btn\": {\n      type: \"Button\",\n      props: { label: \"Reset\", variant: \"danger\" },\n      on: { press: { action: \"reset\" } },\n    },\n    \"milestone-badge\": {\n      type: \"Badge\",\n      props: { label: \"Milestone reached: 10!\", color: \"#10b981\" },\n      visible: { $state: \"/count\", gte: 10 },\n    },\n    \"todos-card\": {\n      type: \"Card\",\n      props: { title: \"Todo List\", subtitle: \"Your tasks\" },\n      children: [\"todos-list\"],\n    },\n    \"todos-list\": {\n      type: \"Stack\",\n      props: { gap: 8, direction: \"vertical\" },\n      repeat: { statePath: \"/todos\", key: \"id\" },\n      children: [\"todo-item\"],\n    },\n    \"todo-item\": {\n      type: \"ListItem\",\n      props: {\n        title: { $item: \"title\" },\n        completed: { $item: \"completed\" },\n      },\n      on: {\n        press: { action: \"toggleItem\", params: { index: { $index: true } } },\n      },\n    },\n    \"input-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Bound Input\",\n        subtitle: \"Type to update state and see reactive text\",\n      },\n      children: [\"input-body\"],\n    },\n    \"input-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"vertical\" },\n      children: [\"name-input\", \"name-display\"],\n    },\n    \"name-input\": {\n      type: \"Input\",\n      props: {\n        value: { $bindState: \"/name\" },\n        placeholder: \"Enter your name…\",\n      },\n    },\n    \"name-display\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/name\" },\n        size: \"md\",\n        color: \"#6b7280\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/svelte/src/main.ts",
    "content": "import { mount } from \"svelte\";\nimport App from \"./App.svelte\";\nimport \"./app.css\";\n\nconst app = mount(App, {\n  target: document.getElementById(\"app\")!,\n});\n\nexport default app;\n"
  },
  {
    "path": "examples/svelte/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "examples/svelte/svelte.config.js",
    "content": "export default {};\n"
  },
  {
    "path": "examples/svelte/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true\n  },\n  \"files\": [\"src/DemoRenderer.svelte\"],\n  \"exclude\": [\"node_modules\"]\n  // \"include\": [\"src/**/*.ts\", \"src/**/*.svelte\"]\n}\n"
  },
  {
    "path": "examples/svelte/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\n\nexport default defineConfig({\n  plugins: [svelte()],\n});\n"
  },
  {
    "path": "examples/svelte-chat/.env.example",
    "content": "# Vercel AI Gateway\n# Automatically authenticated when deployed on Vercel\n# For local development, get your key from https://vercel.com/ai-gateway\nAI_GATEWAY_API_KEY=\n\n# AI Model Configuration\n# Default: anthropic/claude-haiku-4.5\nAI_GATEWAY_MODEL=anthropic/claude-haiku-4.5\n\n# Rate Limiting (Upstash Redis)\n# Optional - rate limiting is disabled when these are not set\nKV_REST_API_URL=\nKV_REST_API_TOKEN=\nRATE_LIMIT_PER_MINUTE=10\nRATE_LIMIT_PER_DAY=100\n"
  },
  {
    "path": "examples/svelte-chat/.gitignore",
    "content": "node_modules\n\n# Output\n.output\n.vercel\n.netlify\n.wrangler\n/.svelte-kit\n/build\n\n# OS\n.DS_Store\nThumbs.db\n\n# Env\n.env\n.env.*\n!.env.example\n!.env.test\n\n# Vite\nvite.config.js.timestamp-*\nvite.config.ts.timestamp-*\n"
  },
  {
    "path": "examples/svelte-chat/.npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": "examples/svelte-chat/CHANGELOG.md",
    "content": "# svelte-chat\n\n## 0.0.6\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/svelte@0.14.1\n\n## 0.0.5\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/svelte@0.14.0\n\n## 0.0.4\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/svelte@0.13.0\n\n## 0.0.3\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/svelte@0.12.1\n\n## 0.0.2\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/svelte@0.12.0\n"
  },
  {
    "path": "examples/svelte-chat/README.md",
    "content": "# sv\n\nEverything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```sh\n# create a new project\nnpx sv create my-app\n```\n\nTo recreate this project with the same configuration:\n\n```sh\n# recreate this project\nnpx sv create --template minimal --types ts --no-install svelte-chat\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```sh\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\n## Building\n\nTo create a production version of your app:\n\n```sh\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.\n"
  },
  {
    "path": "examples/svelte-chat/components.json",
    "content": "{\n\t\"$schema\": \"https://shadcn-svelte.com/schema.json\",\n\t\"tailwind\": {\n\t\t\"css\": \"src/app.css\",\n\t\t\"baseColor\": \"zinc\"\n\t},\n\t\"aliases\": {\n\t\t\"components\": \"$lib/components\",\n\t\t\"utils\": \"$lib/utils\",\n\t\t\"ui\": \"$lib/components/ui\",\n\t\t\"hooks\": \"$lib/hooks\",\n\t\t\"lib\": \"$lib\"\n\t},\n\t\"typescript\": true,\n\t\"registry\": \"https://shadcn-svelte.com/registry\"\n}\n"
  },
  {
    "path": "examples/svelte-chat/package.json",
    "content": "{\n  \"name\": \"svelte-chat\",\n  \"private\": true,\n  \"version\": \"0.0.6\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"prepare\": \"svelte-kit sync || echo ''\",\n    \"check-types\": \"svelte-kit sync && svelte-check --tsconfig ./tsconfig.json\"\n  },\n  \"devDependencies\": {\n    \"@internationalized/date\": \"^3.10.0\",\n    \"@lucide/svelte\": \"^0.561.0\",\n    \"@sveltejs/adapter-auto\": \"^7.0.0\",\n    \"@sveltejs/kit\": \"^2.50.2\",\n    \"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n    \"@tailwindcss/vite\": \"^4.0.0\",\n    \"bits-ui\": \"^2.14.4\",\n    \"svelte\": \"^5.49.2\",\n    \"svelte-check\": \"^4.3.6\",\n    \"tailwind-variants\": \"^3.2.2\",\n    \"tailwindcss\": \"^4.0.0\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.46\",\n    \"@ai-sdk/svelte\": \"^4.0.96\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/svelte\": \"workspace:*\",\n    \"ai\": \"^6.0.86\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-svelte\": \"^0.500.0\",\n    \"tailwind-merge\": \"^3.2.0\",\n    \"@upstash/ratelimit\": \"^2.0.8\",\n    \"@upstash/redis\": \"^1.37.0\",\n    \"zod\": \"4.3.6\"\n  }\n}\n"
  },
  {
    "path": "examples/svelte-chat/src/app.css",
    "content": "@import \"tailwindcss\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n  --radius-2xl: calc(var(--radius) + 8px);\n  --radius-3xl: calc(var(--radius) + 12px);\n  --radius-4xl: calc(var(--radius) + 16px);\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n}\n\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\nbutton {\n  cursor: pointer;\n}\n\n@keyframes shimmer {\n  0% {\n    background-position: -200% 0;\n  }\n  100% {\n    background-position: 200% 0;\n  }\n}\n\n.animate-shimmer {\n  background: linear-gradient(\n    90deg,\n    currentColor 25%,\n    hsl(0 0% 64%) 50%,\n    currentColor 75%\n  );\n  background-size: 200% 100%;\n  -webkit-background-clip: text;\n  background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer 2s ease-in-out infinite;\n}\n"
  },
  {
    "path": "examples/svelte-chat/src/app.d.ts",
    "content": "// See https://svelte.dev/docs/kit/types#app.d.ts\n// for information about these interfaces\ndeclare global {\n  namespace App {\n    // interface Error {}\n    // interface Locals {}\n    // interface PageData {}\n    // interface PageState {}\n    // interface Platform {}\n  }\n}\n\nexport {};\n"
  },
  {
    "path": "examples/svelte-chat/src/app.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\t\t<meta property=\"og:title\" content=\"Svelte Chat Example | json-render\" />\n\t\t<meta property=\"og:image\" content=\"/og-image.png\" />\n\t\t<meta property=\"og:image:width\" content=\"1200\" />\n\t\t<meta property=\"og:image:height\" content=\"630\" />\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" />\n\t\t<meta name=\"twitter:title\" content=\"Svelte Chat Example | json-render\" />\n\t\t<meta name=\"twitter:image\" content=\"/og-image.png\" />\n\t\t%sveltekit.head%\n\t</head>\n\t<body data-sveltekit-preload-data=\"hover\">\n\t\t<div style=\"display: contents\">%sveltekit.body%</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/agent.ts",
    "content": "import { ToolLoopAgent, stepCountIs } from \"ai\";\nimport { createGateway } from \"@ai-sdk/gateway\";\nimport { explorerCatalog } from \"./render/catalog\";\nimport { getWeather } from \"./tools/weather\";\nimport { getGitHubRepo, getGitHubPullRequests } from \"./tools/github\";\nimport { getCryptoPrice, getCryptoPriceHistory } from \"./tools/crypto\";\nimport { getHackerNewsTop } from \"./tools/hackernews\";\nimport { webSearch } from \"./tools/search\";\nimport { env } from \"$env/dynamic/private\";\n\nconst DEFAULT_MODEL = \"anthropic/claude-haiku-4.5\";\n\nconst AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.\n\nWORKFLOW:\n1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.\n2. Respond with a brief, conversational summary of what you found.\n3. Then output the JSONL UI spec wrapped in a \\`\\`\\`spec fence to render a rich visual experience.\n\nRULES:\n- Always call tools FIRST to get real data. Never make up data.\n- Embed the fetched data directly in /state paths so components can reference it.\n- Use Card components to group related information.\n- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.\n- Use Grid for multi-column layouts.\n- Use Metric for key numeric values (temperature, stars, price, etc.).\n- Use Table for lists of items (stories, forecasts, languages, etc.).\n- Use BarChart or LineChart for numeric trends and time-series data.\n- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).\n- Use Tabs when showing multiple categories of data side by side.\n- Use Badge for status indicators.\n- Use Callout for key facts, tips, warnings, or important takeaways.\n- Use Accordion to organize detailed sections the user can expand for deeper reading.\n- Use Timeline for historical events, processes, step-by-step explanations, or milestones.\n- When teaching about a topic, combine multiple component types to create a rich, engaging experience.\n\nDATA BINDING:\n- The state model is the single source of truth. Put fetched data in /state, then reference it with { \"$state\": \"/json/pointer\" } in any prop.\n- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.\n- Scalar binding: \"title\": { \"$state\": \"/quiz/title\" }\n- Array binding: \"items\": { \"$state\": \"/quiz/questions\" } (for Accordion, Timeline, etc.)\n- For Table, BarChart, LineChart, and PieChart, use { \"$state\": \"/path\" } on the data prop to bind read-only data from state.\n- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.\n- Always use the { \"$state\": \"/foo\" } object syntax for data binding.\n\nINTERACTIVITY:\n- You can use visible, repeat, on.press, and $cond/$then/$else freely.\n- visible: Conditionally show/hide elements based on state. e.g. \"visible\": { \"$state\": \"/q1/answer\", \"eq\": \"a\" }\n- repeat: Iterate over state arrays. e.g. \"repeat\": { \"statePath\": \"/items\" }\n- on.press: Trigger actions on button clicks. e.g. \"on\": { \"press\": { \"action\": \"setState\", \"params\": { \"statePath\": \"/submitted\", \"value\": true } } }\n- $cond/$then/$else: Conditional prop values. e.g. { \"$cond\": { \"$state\": \"/correct\" }, \"$then\": \"Correct!\", \"$else\": \"Try again\" }\n\nBUILT-IN ACTIONS (use with on.press):\n- setState: Set a value at a state path. params: { statePath: \"/foo\", value: \"bar\" }\n- pushState: Append to an array. params: { statePath: \"/items\", value: { ... } }\n- removeState: Remove by index. params: { statePath: \"/items\", index: 0 }\n\nINPUT COMPONENTS:\n- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.\n- SelectInput: Dropdown select. Writes selected value to statePath automatically.\n- TextInput: Text input field. Writes entered value to statePath automatically.\n- Button: Clickable button. Use on.press to trigger actions.\n\n${explorerCatalog.prompt({\n  mode: \"inline\",\n  customRules: [\n    \"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.\",\n    \"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.\",\n    \"Use Metric components for key numbers instead of plain Text.\",\n    \"Put chart data arrays in /state and reference them with { $state: '/path' } on the data prop.\",\n    \"Keep the UI clean and information-dense — no excessive padding or empty space.\",\n    \"For educational prompts ('teach me about', 'explain', 'what is'), use a mix of Callout, Accordion, Timeline, and charts to make the content visually rich.\",\n  ],\n})}`;\n\nexport const gateway = createGateway({ apiKey: env.AI_GATEWAY_API_KEY });\n\nexport const agent = new ToolLoopAgent({\n  model: gateway(env.AI_GATEWAY_MODEL || DEFAULT_MODEL),\n  instructions: AGENT_INSTRUCTIONS,\n  tools: {\n    getWeather,\n    getGitHubRepo,\n    getGitHubPullRequests,\n    getCryptoPrice,\n    getCryptoPriceHistory,\n    getHackerNewsTop,\n    webSearch,\n  },\n  stopWhen: stepCountIs(5),\n  temperature: 0.7,\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/accordion/accordion-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Accordion as AccordionPrimitive } from \"bits-ui\";\n\timport { cn, type WithoutChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithoutChild<AccordionPrimitive.ContentProps> = $props();\n</script>\n\n<AccordionPrimitive.Content\n\tbind:ref\n\tdata-slot=\"accordion-content\"\n\tclass=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n\t{...restProps}\n>\n\t<div class={cn(\"pt-0 pb-4\", className)}>\n\t\t{@render children?.()}\n\t</div>\n</AccordionPrimitive.Content>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/accordion/accordion-item.svelte",
    "content": "<script lang=\"ts\">\n\timport { Accordion as AccordionPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: AccordionPrimitive.ItemProps = $props();\n</script>\n\n<AccordionPrimitive.Item\n\tbind:ref\n\tdata-slot=\"accordion-item\"\n\tclass={cn(\"border-b last:border-b-0\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/accordion/accordion-trigger.svelte",
    "content": "<script lang=\"ts\">\n\timport { Accordion as AccordionPrimitive } from \"bits-ui\";\n\timport ChevronDownIcon from \"@lucide/svelte/icons/chevron-down\";\n\timport { cn, type WithoutChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tlevel = 3,\n\t\tchildren,\n\t\t...restProps\n\t}: WithoutChild<AccordionPrimitive.TriggerProps> & {\n\t\tlevel?: AccordionPrimitive.HeaderProps[\"level\"];\n\t} = $props();\n</script>\n\n<AccordionPrimitive.Header {level} class=\"flex\">\n\t<AccordionPrimitive.Trigger\n\t\tdata-slot=\"accordion-trigger\"\n\t\tbind:ref\n\t\tclass={cn(\n\t\t\t\"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n\t\t\tclassName\n\t\t)}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t\t<ChevronDownIcon\n\t\t\tclass=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\"\n\t\t/>\n\t</AccordionPrimitive.Trigger>\n</AccordionPrimitive.Header>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/accordion/accordion.svelte",
    "content": "<script lang=\"ts\">\n\timport { Accordion as AccordionPrimitive } from \"bits-ui\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tvalue = $bindable(),\n\t\t...restProps\n\t}: AccordionPrimitive.RootProps = $props();\n</script>\n\n<AccordionPrimitive.Root\n\tbind:ref\n\tbind:value={value as never}\n\tdata-slot=\"accordion\"\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/accordion/index.ts",
    "content": "import Root from \"./accordion.svelte\";\nimport Content from \"./accordion-content.svelte\";\nimport Item from \"./accordion-item.svelte\";\nimport Trigger from \"./accordion-trigger.svelte\";\n\nexport {\n  Root,\n  Content,\n  Item,\n  Trigger,\n  //\n  Root as Accordion,\n  Content as AccordionContent,\n  Item as AccordionItem,\n  Trigger as AccordionTrigger,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/alert/alert-description.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"alert-description\"\n\tclass={cn(\n\t\t\"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/alert/alert-title.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"alert-title\"\n\tclass={cn(\"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/alert/alert.svelte",
    "content": "<script lang=\"ts\" module>\n\timport { type VariantProps, tv } from \"tailwind-variants\";\n\n\texport const alertVariants = tv({\n\t\tbase: \"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-card text-card-foreground\",\n\t\t\t\tdestructive:\n\t\t\t\t\t\"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t},\n\t});\n\n\texport type AlertVariant = VariantProps<typeof alertVariants>[\"variant\"];\n</script>\n\n<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvariant = \"default\",\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {\n\t\tvariant?: AlertVariant;\n\t} = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"alert\"\n\tclass={cn(alertVariants({ variant }), className)}\n\t{...restProps}\n\trole=\"alert\"\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/alert/index.ts",
    "content": "import Root from \"./alert.svelte\";\nimport Description from \"./alert-description.svelte\";\nimport Title from \"./alert-title.svelte\";\nexport { alertVariants, type AlertVariant } from \"./alert.svelte\";\n\nexport {\n  Root,\n  Description,\n  Title,\n  //\n  Root as Alert,\n  Description as AlertDescription,\n  Title as AlertTitle,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/badge/badge.svelte",
    "content": "<script lang=\"ts\" module>\n\timport { type VariantProps, tv } from \"tailwind-variants\";\n\n\texport const badgeVariants = tv({\n\t\tbase: \"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3\",\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault:\n\t\t\t\t\t\"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent\",\n\t\t\t\tsecondary:\n\t\t\t\t\t\"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent\",\n\t\t\t\tdestructive:\n\t\t\t\t\t\"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white\",\n\t\t\t\toutline: \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t},\n\t});\n\n\texport type BadgeVariant = VariantProps<typeof badgeVariants>[\"variant\"];\n</script>\n\n<script lang=\"ts\">\n\timport type { HTMLAnchorAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\thref,\n\t\tclass: className,\n\t\tvariant = \"default\",\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAnchorAttributes> & {\n\t\tvariant?: BadgeVariant;\n\t} = $props();\n</script>\n\n<svelte:element\n\tthis={href ? \"a\" : \"span\"}\n\tbind:this={ref}\n\tdata-slot=\"badge\"\n\t{href}\n\tclass={cn(badgeVariants({ variant }), className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</svelte:element>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/badge/index.ts",
    "content": "export { default as Badge } from \"./badge.svelte\";\nexport { badgeVariants, type BadgeVariant } from \"./badge.svelte\";\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/button/button.svelte",
    "content": "<script lang=\"ts\" module>\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAnchorAttributes, HTMLButtonAttributes } from \"svelte/elements\";\n\timport { type VariantProps, tv } from \"tailwind-variants\";\n\n\texport const buttonVariants = tv({\n\t\tbase: \"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs\",\n\t\t\t\tdestructive:\n\t\t\t\t\t\"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs\",\n\t\t\t\toutline:\n\t\t\t\t\t\"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs\",\n\t\t\t\tsecondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs\",\n\t\t\t\tghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n\t\t\t\tlink: \"text-primary underline-offset-4 hover:underline\",\n\t\t\t},\n\t\t\tsize: {\n\t\t\t\tdefault: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n\t\t\t\tsm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n\t\t\t\tlg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n\t\t\t\ticon: \"size-9\",\n\t\t\t\t\"icon-sm\": \"size-8\",\n\t\t\t\t\"icon-lg\": \"size-10\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: {\n\t\t\tvariant: \"default\",\n\t\t\tsize: \"default\",\n\t\t},\n\t});\n\n\texport type ButtonVariant = VariantProps<typeof buttonVariants>[\"variant\"];\n\texport type ButtonSize = VariantProps<typeof buttonVariants>[\"size\"];\n\n\texport type ButtonProps = WithElementRef<HTMLButtonAttributes> &\n\t\tWithElementRef<HTMLAnchorAttributes> & {\n\t\t\tvariant?: ButtonVariant;\n\t\t\tsize?: ButtonSize;\n\t\t};\n</script>\n\n<script lang=\"ts\">\n\tlet {\n\t\tclass: className,\n\t\tvariant = \"default\",\n\t\tsize = \"default\",\n\t\tref = $bindable(null),\n\t\thref = undefined,\n\t\ttype = \"button\",\n\t\tdisabled,\n\t\tchildren,\n\t\t...restProps\n\t}: ButtonProps = $props();\n</script>\n\n{#if href}\n\t<a\n\t\tbind:this={ref}\n\t\tdata-slot=\"button\"\n\t\tclass={cn(buttonVariants({ variant, size }), className)}\n\t\thref={disabled ? undefined : href}\n\t\taria-disabled={disabled}\n\t\trole={disabled ? \"link\" : undefined}\n\t\ttabindex={disabled ? -1 : undefined}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t</a>\n{:else}\n\t<button\n\t\tbind:this={ref}\n\t\tdata-slot=\"button\"\n\t\tclass={cn(buttonVariants({ variant, size }), className)}\n\t\t{type}\n\t\t{disabled}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t</button>\n{/if}\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/button/index.ts",
    "content": "import Root, {\n  type ButtonProps,\n  type ButtonSize,\n  type ButtonVariant,\n  buttonVariants,\n} from \"./button.svelte\";\n\nexport {\n  Root,\n  type ButtonProps as Props,\n  //\n  Root as Button,\n  buttonVariants,\n  type ButtonProps,\n  type ButtonSize,\n  type ButtonVariant,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-action.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"card-action\"\n\tclass={cn(\"col-start-2 row-span-2 row-start-1 self-start justify-self-end\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-content.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div bind:this={ref} data-slot=\"card-content\" class={cn(\"px-6\", className)} {...restProps}>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-description.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();\n</script>\n\n<p\n\tbind:this={ref}\n\tdata-slot=\"card-description\"\n\tclass={cn(\"text-muted-foreground text-sm\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</p>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-footer.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"card-footer\"\n\tclass={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-header.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"card-header\"\n\tclass={cn(\n\t\t\"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card-title.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"card-title\"\n\tclass={cn(\"leading-none font-semibold\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/card.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"card\"\n\tclass={cn(\n\t\t\"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/card/index.ts",
    "content": "import Root from \"./card.svelte\";\nimport Content from \"./card-content.svelte\";\nimport Description from \"./card-description.svelte\";\nimport Footer from \"./card-footer.svelte\";\nimport Header from \"./card-header.svelte\";\nimport Title from \"./card-title.svelte\";\nimport Action from \"./card-action.svelte\";\n\nexport {\n  Root,\n  Content,\n  Description,\n  Footer,\n  Header,\n  Title,\n  Action,\n  //\n  Root as Card,\n  Content as CardContent,\n  Description as CardDescription,\n  Footer as CardFooter,\n  Header as CardHeader,\n  Title as CardTitle,\n  Action as CardAction,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/input/index.ts",
    "content": "import Root from \"./input.svelte\";\n\nexport {\n  Root,\n  //\n  Root as Input,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/input/input.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLInputAttributes, HTMLInputTypeAttribute } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\ttype InputType = Exclude<HTMLInputTypeAttribute, \"file\">;\n\n\ttype Props = WithElementRef<\n\t\tOmit<HTMLInputAttributes, \"type\"> &\n\t\t\t({ type: \"file\"; files?: FileList } | { type?: InputType; files?: undefined })\n\t>;\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tvalue = $bindable(),\n\t\ttype,\n\t\tfiles = $bindable(),\n\t\tclass: className,\n\t\t\"data-slot\": dataSlot = \"input\",\n\t\t...restProps\n\t}: Props = $props();\n</script>\n\n{#if type === \"file\"}\n\t<input\n\t\tbind:this={ref}\n\t\tdata-slot={dataSlot}\n\t\tclass={cn(\n\t\t\t\"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n\t\t\t\"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n\t\t\t\"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\t\t\tclassName\n\t\t)}\n\t\ttype=\"file\"\n\t\tbind:files\n\t\tbind:value\n\t\t{...restProps}\n\t/>\n{:else}\n\t<input\n\t\tbind:this={ref}\n\t\tdata-slot={dataSlot}\n\t\tclass={cn(\n\t\t\t\"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n\t\t\t\"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n\t\t\t\"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n\t\t\tclassName\n\t\t)}\n\t\t{type}\n\t\tbind:value\n\t\t{...restProps}\n\t/>\n{/if}\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/label/index.ts",
    "content": "import Root from \"./label.svelte\";\n\nexport {\n  Root,\n  //\n  Root as Label,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/label/label.svelte",
    "content": "<script lang=\"ts\">\n\timport { Label as LabelPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: LabelPrimitive.RootProps = $props();\n</script>\n\n<LabelPrimitive.Root\n\tbind:ref\n\tdata-slot=\"label\"\n\tclass={cn(\n\t\t\"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/progress/index.ts",
    "content": "import Root from \"./progress.svelte\";\n\nexport {\n  Root,\n  //\n  Root as Progress,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/progress/progress.svelte",
    "content": "<script lang=\"ts\">\n\timport { Progress as ProgressPrimitive } from \"bits-ui\";\n\timport { cn, type WithoutChildrenOrChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tmax = 100,\n\t\tvalue,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<ProgressPrimitive.RootProps> = $props();\n</script>\n\n<ProgressPrimitive.Root\n\tbind:ref\n\tdata-slot=\"progress\"\n\tclass={cn(\"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\", className)}\n\t{value}\n\t{max}\n\t{...restProps}\n>\n\t<div\n\t\tdata-slot=\"progress-indicator\"\n\t\tclass=\"bg-primary h-full w-full flex-1 transition-all\"\n\t\tstyle=\"transform: translateX(-{100 - (100 * (value ?? 0)) / (max ?? 1)}%)\"\n\t></div>\n</ProgressPrimitive.Root>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/radio-group/index.ts",
    "content": "import Root from \"./radio-group.svelte\";\nimport Item from \"./radio-group-item.svelte\";\n\nexport {\n  Root,\n  Item,\n  //\n  Root as RadioGroup,\n  Item as RadioGroupItem,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/radio-group/radio-group-item.svelte",
    "content": "<script lang=\"ts\">\n\timport { RadioGroup as RadioGroupPrimitive } from \"bits-ui\";\n\timport CircleIcon from \"@lucide/svelte/icons/circle\";\n\timport { cn, type WithoutChildrenOrChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();\n</script>\n\n<RadioGroupPrimitive.Item\n\tbind:ref\n\tdata-slot=\"radio-group-item\"\n\tclass={cn(\n\t\t\"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{#snippet children({ checked })}\n\t\t<div data-slot=\"radio-group-indicator\" class=\"relative flex items-center justify-center\">\n\t\t\t{#if checked}\n\t\t\t\t<CircleIcon\n\t\t\t\t\tclass=\"fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2\"\n\t\t\t\t/>\n\t\t\t{/if}\n\t\t</div>\n\t{/snippet}\n</RadioGroupPrimitive.Item>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/radio-group/radio-group.svelte",
    "content": "<script lang=\"ts\">\n\timport { RadioGroup as RadioGroupPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvalue = $bindable(\"\"),\n\t\t...restProps\n\t}: RadioGroupPrimitive.RootProps = $props();\n</script>\n\n<RadioGroupPrimitive.Root\n\tbind:ref\n\tbind:value\n\tdata-slot=\"radio-group\"\n\tclass={cn(\"grid gap-3\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/index.ts",
    "content": "import Root from \"./select.svelte\";\nimport Group from \"./select-group.svelte\";\nimport Label from \"./select-label.svelte\";\nimport Item from \"./select-item.svelte\";\nimport Content from \"./select-content.svelte\";\nimport Trigger from \"./select-trigger.svelte\";\nimport Separator from \"./select-separator.svelte\";\nimport ScrollDownButton from \"./select-scroll-down-button.svelte\";\nimport ScrollUpButton from \"./select-scroll-up-button.svelte\";\nimport GroupHeading from \"./select-group-heading.svelte\";\nimport Portal from \"./select-portal.svelte\";\n\nexport {\n  Root,\n  Group,\n  Label,\n  Item,\n  Content,\n  Trigger,\n  Separator,\n  ScrollDownButton,\n  ScrollUpButton,\n  GroupHeading,\n  Portal,\n  //\n  Root as Select,\n  Group as SelectGroup,\n  Label as SelectLabel,\n  Item as SelectItem,\n  Content as SelectContent,\n  Trigger as SelectTrigger,\n  Separator as SelectSeparator,\n  ScrollDownButton as SelectScrollDownButton,\n  ScrollUpButton as SelectScrollUpButton,\n  GroupHeading as SelectGroupHeading,\n  Portal as SelectPortal,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport SelectPortal from \"./select-portal.svelte\";\n\timport SelectScrollUpButton from \"./select-scroll-up-button.svelte\";\n\timport SelectScrollDownButton from \"./select-scroll-down-button.svelte\";\n\timport { cn, type WithoutChild } from \"$lib/utils.js\";\n\timport type { ComponentProps } from \"svelte\";\n\timport type { WithoutChildrenOrChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tsideOffset = 4,\n\t\tportalProps,\n\t\tchildren,\n\t\tpreventScroll = true,\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.ContentProps> & {\n\t\tportalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;\n\t} = $props();\n</script>\n\n<SelectPortal {...portalProps}>\n\t<SelectPrimitive.Content\n\t\tbind:ref\n\t\t{sideOffset}\n\t\t{preventScroll}\n\t\tdata-slot=\"select-content\"\n\t\tclass={cn(\n\t\t\t\"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n\t\t\tclassName\n\t\t)}\n\t\t{...restProps}\n\t>\n\t\t<SelectScrollUpButton />\n\t\t<SelectPrimitive.Viewport\n\t\t\tclass={cn(\n\t\t\t\t\"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1\"\n\t\t\t)}\n\t\t>\n\t\t\t{@render children?.()}\n\t\t</SelectPrimitive.Viewport>\n\t\t<SelectScrollDownButton />\n\t</SelectPrimitive.Content>\n</SelectPortal>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-group-heading.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\timport type { ComponentProps } from \"svelte\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();\n</script>\n\n<SelectPrimitive.GroupHeading\n\tbind:ref\n\tdata-slot=\"select-group-heading\"\n\tclass={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</SelectPrimitive.GroupHeading>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-group.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\n\tlet { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();\n</script>\n\n<SelectPrimitive.Group bind:ref data-slot=\"select-group\" {...restProps} />\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-item.svelte",
    "content": "<script lang=\"ts\">\n\timport CheckIcon from \"@lucide/svelte/icons/check\";\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport { cn, type WithoutChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tvalue,\n\t\tlabel,\n\t\tchildren: childrenProp,\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.ItemProps> = $props();\n</script>\n\n<SelectPrimitive.Item\n\tbind:ref\n\t{value}\n\tdata-slot=\"select-item\"\n\tclass={cn(\n\t\t\"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{#snippet children({ selected, highlighted })}\n\t\t<span class=\"absolute end-2 flex size-3.5 items-center justify-center\">\n\t\t\t{#if selected}\n\t\t\t\t<CheckIcon class=\"size-4\" />\n\t\t\t{/if}\n\t\t</span>\n\t\t{#if childrenProp}\n\t\t\t{@render childrenProp({ selected, highlighted })}\n\t\t{:else}\n\t\t\t{label || value}\n\t\t{/if}\n\t{/snippet}\n</SelectPrimitive.Item>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-label.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();\n</script>\n\n<div\n\tbind:this={ref}\n\tdata-slot=\"select-label\"\n\tclass={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-portal.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\n\tlet { ...restProps }: SelectPrimitive.PortalProps = $props();\n</script>\n\n<SelectPrimitive.Portal {...restProps} />\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-scroll-down-button.svelte",
    "content": "<script lang=\"ts\">\n\timport ChevronDownIcon from \"@lucide/svelte/icons/chevron-down\";\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport { cn, type WithoutChildrenOrChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();\n</script>\n\n<SelectPrimitive.ScrollDownButton\n\tbind:ref\n\tdata-slot=\"select-scroll-down-button\"\n\tclass={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n\t{...restProps}\n>\n\t<ChevronDownIcon class=\"size-4\" />\n</SelectPrimitive.ScrollDownButton>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-scroll-up-button.svelte",
    "content": "<script lang=\"ts\">\n\timport ChevronUpIcon from \"@lucide/svelte/icons/chevron-up\";\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport { cn, type WithoutChildrenOrChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();\n</script>\n\n<SelectPrimitive.ScrollUpButton\n\tbind:ref\n\tdata-slot=\"select-scroll-up-button\"\n\tclass={cn(\"flex cursor-default items-center justify-center py-1\", className)}\n\t{...restProps}\n>\n\t<ChevronUpIcon class=\"size-4\" />\n</SelectPrimitive.ScrollUpButton>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-separator.svelte",
    "content": "<script lang=\"ts\">\n\timport type { Separator as SeparatorPrimitive } from \"bits-ui\";\n\timport { Separator } from \"$lib/components/ui/separator/index.js\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: SeparatorPrimitive.RootProps = $props();\n</script>\n\n<Separator\n\tbind:ref\n\tdata-slot=\"select-separator\"\n\tclass={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select-trigger.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\timport ChevronDownIcon from \"@lucide/svelte/icons/chevron-down\";\n\timport { cn, type WithoutChild } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\tsize = \"default\",\n\t\t...restProps\n\t}: WithoutChild<SelectPrimitive.TriggerProps> & {\n\t\tsize?: \"sm\" | \"default\";\n\t} = $props();\n</script>\n\n<SelectPrimitive.Trigger\n\tbind:ref\n\tdata-slot=\"select-trigger\"\n\tdata-size={size}\n\tclass={cn(\n\t\t\"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n\t<ChevronDownIcon class=\"size-4 opacity-50\" />\n</SelectPrimitive.Trigger>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/select/select.svelte",
    "content": "<script lang=\"ts\">\n\timport { Select as SelectPrimitive } from \"bits-ui\";\n\n\tlet {\n\t\topen = $bindable(false),\n\t\tvalue = $bindable(),\n\t\t...restProps\n\t}: SelectPrimitive.RootProps = $props();\n</script>\n\n<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/separator/index.ts",
    "content": "import Root from \"./separator.svelte\";\n\nexport {\n  Root,\n  //\n  Root as Separator,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/separator/separator.svelte",
    "content": "<script lang=\"ts\">\n\timport { Separator as SeparatorPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t\"data-slot\": dataSlot = \"separator\",\n\t\t...restProps\n\t}: SeparatorPrimitive.RootProps = $props();\n</script>\n\n<SeparatorPrimitive.Root\n\tbind:ref\n\tdata-slot={dataSlot}\n\tclass={cn(\n\t\t\"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/index.ts",
    "content": "import Root from \"./table.svelte\";\nimport Body from \"./table-body.svelte\";\nimport Caption from \"./table-caption.svelte\";\nimport Cell from \"./table-cell.svelte\";\nimport Footer from \"./table-footer.svelte\";\nimport Head from \"./table-head.svelte\";\nimport Header from \"./table-header.svelte\";\nimport Row from \"./table-row.svelte\";\n\nexport {\n  Root,\n  Body,\n  Caption,\n  Cell,\n  Footer,\n  Head,\n  Header,\n  Row,\n  //\n  Root as Table,\n  Body as TableBody,\n  Caption as TableCaption,\n  Cell as TableCell,\n  Footer as TableFooter,\n  Head as TableHead,\n  Header as TableHeader,\n  Row as TableRow,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-body.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<tbody\n\tbind:this={ref}\n\tdata-slot=\"table-body\"\n\tclass={cn(\"[&_tr:last-child]:border-0\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</tbody>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-caption.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();\n</script>\n\n<caption\n\tbind:this={ref}\n\tdata-slot=\"table-caption\"\n\tclass={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</caption>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-cell.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLTdAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLTdAttributes> = $props();\n</script>\n\n<td\n\tbind:this={ref}\n\tdata-slot=\"table-cell\"\n\tclass={cn(\n\t\t\"bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</td>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-footer.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<tfoot\n\tbind:this={ref}\n\tdata-slot=\"table-footer\"\n\tclass={cn(\"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</tfoot>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-head.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLThAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLThAttributes> = $props();\n</script>\n\n<th\n\tbind:this={ref}\n\tdata-slot=\"table-head\"\n\tclass={cn(\n\t\t\"text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</th>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-header.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();\n</script>\n\n<thead\n\tbind:this={ref}\n\tdata-slot=\"table-header\"\n\tclass={cn(\"[&_tr]:border-b\", className)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</thead>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table-row.svelte",
    "content": "<script lang=\"ts\">\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();\n</script>\n\n<tr\n\tbind:this={ref}\n\tdata-slot=\"table-row\"\n\tclass={cn(\n\t\t\"hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n\t\tclassName\n\t)}\n\t{...restProps}\n>\n\t{@render children?.()}\n</tr>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/table/table.svelte",
    "content": "<script lang=\"ts\">\n\timport type { HTMLTableAttributes } from \"svelte/elements\";\n\timport { cn, type WithElementRef } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\tchildren,\n\t\t...restProps\n\t}: WithElementRef<HTMLTableAttributes> = $props();\n</script>\n\n<div data-slot=\"table-container\" class=\"relative w-full overflow-x-auto\">\n\t<table\n\t\tbind:this={ref}\n\t\tdata-slot=\"table\"\n\t\tclass={cn(\"w-full caption-bottom text-sm\", className)}\n\t\t{...restProps}\n\t>\n\t\t{@render children?.()}\n\t</table>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/tabs/index.ts",
    "content": "import Root from \"./tabs.svelte\";\nimport Content from \"./tabs-content.svelte\";\nimport List from \"./tabs-list.svelte\";\nimport Trigger from \"./tabs-trigger.svelte\";\n\nexport {\n  Root,\n  Content,\n  List,\n  Trigger,\n  //\n  Root as Tabs,\n  Content as TabsContent,\n  List as TabsList,\n  Trigger as TabsTrigger,\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/tabs/tabs-content.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: TabsPrimitive.ContentProps = $props();\n</script>\n\n<TabsPrimitive.Content\n\tbind:ref\n\tdata-slot=\"tabs-content\"\n\tclass={cn(\"flex-1 outline-none\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/tabs/tabs-list.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: TabsPrimitive.ListProps = $props();\n</script>\n\n<TabsPrimitive.List\n\tbind:ref\n\tdata-slot=\"tabs-list\"\n\tclass={cn(\n\t\t\"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/tabs/tabs-trigger.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tclass: className,\n\t\t...restProps\n\t}: TabsPrimitive.TriggerProps = $props();\n</script>\n\n<TabsPrimitive.Trigger\n\tbind:ref\n\tdata-slot=\"tabs-trigger\"\n\tclass={cn(\n\t\t\"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n\t\tclassName\n\t)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/components/ui/tabs/tabs.svelte",
    "content": "<script lang=\"ts\">\n\timport { Tabs as TabsPrimitive } from \"bits-ui\";\n\timport { cn } from \"$lib/utils.js\";\n\n\tlet {\n\t\tref = $bindable(null),\n\t\tvalue = $bindable(\"\"),\n\t\tclass: className,\n\t\t...restProps\n\t}: TabsPrimitive.RootProps = $props();\n</script>\n\n<TabsPrimitive.Root\n\tbind:ref\n\tbind:value\n\tdata-slot=\"tabs\"\n\tclass={cn(\"flex flex-col gap-2\", className)}\n\t{...restProps}\n/>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/index.ts",
    "content": "// place files you want to import through the `$lib` alias in this folder.\nexport { explorerCatalog } from \"./render/catalog\";\nexport { registry } from \"./render/registry\";\nexport { agent } from \"./agent\";\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/rate-limit.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport { Redis } from \"@upstash/redis\";\n\n// Lazy initialization to avoid errors when Redis env vars are not configured\nlet _minuteRateLimit: Ratelimit | null = null;\nlet _dailyRateLimit: Ratelimit | null = null;\n\nfunction getRedis(): Redis | null {\n  const url = process.env.KV_REST_API_URL;\n  const token = process.env.KV_REST_API_TOKEN;\n\n  if (!url || !token) {\n    return null;\n  }\n\n  return new Redis({ url, token });\n}\n\n// No-op rate limiter for when Redis is not configured\nconst noopRateLimiter = {\n  limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),\n};\n\nconst MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;\nconst DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;\n\n// Requests per minute (sliding window)\nexport const minuteRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_minuteRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _minuteRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, \"1 m\"),\n        prefix: \"ratelimit:svelte-chat:minute\",\n      });\n    }\n    return _minuteRateLimit.limit(identifier);\n  },\n};\n\n// Requests per day (fixed window)\nexport const dailyRateLimit = {\n  limit: async (identifier: string) => {\n    if (!_dailyRateLimit) {\n      const redis = getRedis();\n      if (!redis) return noopRateLimiter.limit();\n      _dailyRateLimit = new Ratelimit({\n        redis,\n        limiter: Ratelimit.fixedWindow(DAILY_LIMIT, \"1 d\"),\n        prefix: \"ratelimit:svelte-chat:daily\",\n      });\n    }\n    return _dailyRateLimit.limit(identifier);\n  },\n};\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/Renderer.svelte",
    "content": "<script lang=\"ts\">\n  import { Renderer, JsonUIProvider } from \"@json-render/svelte\";\n  import type { Spec } from \"@json-render/svelte\";\n  import { registry } from \"./registry\";\n\n  interface Props {\n    spec: Spec;\n    loading?: boolean;\n  }\n\n  let { spec, loading = false }: Props = $props();\n</script>\n\n<JsonUIProvider initialState={spec.state}>\n  <Renderer {spec} {registry} {loading} />\n</JsonUIProvider>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/catalog.ts",
    "content": "import { schema } from \"@json-render/svelte/schema\";\nimport { z } from \"zod\";\n\n/**\n * json-render + AI SDK Example Catalog (Svelte)\n *\n * Components for rendering data dashboards generated by the ToolLoopAgent.\n * Data flows in through tools (weather, GitHub, crypto, HN), not user actions.\n */\nexport const explorerCatalog = schema.createCatalog({\n  components: {\n    // Layout\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n        gap: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n        wrap: z.boolean().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Flex layout container\",\n      example: { direction: \"vertical\", gap: \"md\", wrap: null },\n    },\n\n    Card: {\n      props: z.object({\n        title: z.string().nullable(),\n        description: z.string().nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Card container with optional title and description\",\n      example: { title: \"Weather\", description: \"Current conditions\" },\n    },\n\n    Grid: {\n      props: z.object({\n        columns: z.enum([\"1\", \"2\", \"3\", \"4\"]).nullable(),\n        gap: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Responsive grid layout container\",\n      example: { columns: \"3\", gap: \"md\" },\n    },\n\n    // Typography\n    Heading: {\n      props: z.object({\n        text: z.string(),\n        level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      }),\n      description: \"Section heading\",\n      example: { text: \"Data Explorer\", level: \"h1\" },\n    },\n\n    Text: {\n      props: z.object({\n        content: z.string(),\n        muted: z.boolean().nullable(),\n      }),\n      description: \"Text content\",\n      example: { content: \"Here is your data overview.\" },\n    },\n\n    // Data display\n    Badge: {\n      props: z.object({\n        text: z.string(),\n        variant: z\n          .enum([\"default\", \"secondary\", \"destructive\", \"outline\"])\n          .nullable(),\n      }),\n      description: \"Status badge\",\n      example: { text: \"Live\", variant: \"default\" },\n    },\n\n    Alert: {\n      props: z.object({\n        variant: z.enum([\"default\", \"destructive\"]).nullable(),\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"Alert or info message\",\n    },\n\n    Separator: {\n      props: z.object({}),\n      description: \"Visual divider\",\n    },\n\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        detail: z.string().nullable(),\n        trend: z.enum([\"up\", \"down\", \"neutral\"]).nullable(),\n      }),\n      description:\n        \"Single metric display with label, value, and optional trend indicator\",\n      example: {\n        label: \"Temperature\",\n        value: \"72F\",\n        detail: \"Feels like 68F\",\n        trend: \"up\",\n      },\n    },\n\n    Table: {\n      props: z.object({\n        data: z.array(z.record(z.string(), z.unknown())),\n        columns: z.array(\n          z.object({\n            key: z.string(),\n            label: z.string(),\n          }),\n        ),\n        emptyMessage: z.string().nullable(),\n      }),\n      description:\n        'Data table. Use { \"$state\": \"/path\" } to bind read-only data from state.',\n      example: {\n        data: { $state: \"/stories\" },\n        columns: [\n          { key: \"title\", label: \"Title\" },\n          { key: \"score\", label: \"Score\" },\n        ],\n      },\n    },\n\n    Link: {\n      props: z.object({\n        text: z.string(),\n        href: z.string(),\n      }),\n      description: \"External link that opens in a new tab\",\n      example: { text: \"View on GitHub\", href: \"https://github.com\" },\n    },\n\n    // Charts\n    BarChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Bar chart visualization. Use { \"$state\": \"/path\" } to bind read-only data. xKey is the category field, yKey is the numeric value field.',\n    },\n\n    LineChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        xKey: z.string(),\n        yKey: z.string(),\n        aggregate: z.enum([\"sum\", \"count\", \"avg\"]).nullable(),\n        color: z.string().nullable(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Line chart visualization. Use { \"$state\": \"/path\" } to bind read-only data. xKey is the x-axis field, yKey is the numeric value field.',\n    },\n\n    // Interactive\n    Tabs: {\n      props: z.object({\n        defaultValue: z.string().nullable(),\n        tabs: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      slots: [\"default\"],\n      description: \"Tabbed content container\",\n    },\n\n    TabContent: {\n      props: z.object({\n        value: z.string(),\n      }),\n      slots: [\"default\"],\n      description: \"Content for a specific tab\",\n    },\n\n    Progress: {\n      props: z.object({\n        value: z.number(),\n        max: z.number().nullable(),\n      }),\n      description: \"Progress bar\",\n    },\n\n    Skeleton: {\n      props: z.object({\n        width: z.string().nullable(),\n        height: z.string().nullable(),\n      }),\n      description: \"Loading placeholder\",\n    },\n\n    // Educational / Rich content\n    Callout: {\n      props: z.object({\n        type: z.enum([\"info\", \"tip\", \"warning\", \"important\"]).nullable(),\n        title: z.string().nullable(),\n        content: z.string(),\n      }),\n      description:\n        \"Highlighted callout box for tips, warnings, notes, or key information\",\n      example: {\n        type: \"tip\",\n        title: \"Did you know?\",\n        content: \"The sun is about 93 million miles from Earth.\",\n      },\n    },\n\n    Accordion: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            title: z.string(),\n            content: z.string(),\n          }),\n        ),\n        type: z.enum([\"single\", \"multiple\"]).nullable(),\n      }),\n      description:\n        \"Collapsible accordion sections for organizing detailed content\",\n      example: {\n        items: [{ title: \"Overview\", content: \"A brief introduction.\" }],\n        type: \"multiple\",\n      },\n    },\n\n    Timeline: {\n      props: z.object({\n        items: z.array(\n          z.object({\n            title: z.string(),\n            description: z.string().nullable(),\n            date: z.string().nullable(),\n            status: z.enum([\"completed\", \"current\", \"upcoming\"]).nullable(),\n          }),\n        ),\n      }),\n      description:\n        \"Vertical timeline showing ordered events, steps, or historical milestones\",\n      example: {\n        items: [\n          {\n            title: \"Discovery\",\n            description: \"Initial breakthrough\",\n            date: \"1905\",\n            status: \"completed\",\n          },\n        ],\n      },\n    },\n\n    PieChart: {\n      props: z.object({\n        title: z.string().nullable(),\n        data: z.array(z.record(z.string(), z.unknown())),\n        nameKey: z.string(),\n        valueKey: z.string(),\n        height: z.number().nullable(),\n      }),\n      description:\n        'Pie/donut chart for proportional data. Use { \"$state\": \"/path\" } to bind read-only data. nameKey is the label field, valueKey is the numeric value field.',\n    },\n\n    // Interactive / Input\n    RadioGroup: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      description:\n        'Radio button group for single selection. Use { \"$bindState\": \"/path\" } for two-way binding.',\n      example: {\n        label: \"Choose one\",\n        value: { $bindState: \"/answer\" },\n        options: [\n          { value: \"a\", label: \"Option A\" },\n          { value: \"b\", label: \"Option B\" },\n        ],\n      },\n    },\n\n    SelectInput: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        options: z.array(\n          z.object({\n            value: z.string(),\n            label: z.string(),\n          }),\n        ),\n      }),\n      description:\n        'Dropdown select input. Use { \"$bindState\": \"/path\" } for two-way binding.',\n      example: {\n        label: \"Country\",\n        value: { $bindState: \"/selectedCountry\" },\n        placeholder: \"Select a country\",\n        options: [\n          { value: \"us\", label: \"United States\" },\n          { value: \"uk\", label: \"United Kingdom\" },\n        ],\n      },\n    },\n\n    TextInput: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n        type: z.enum([\"text\", \"email\", \"number\", \"password\", \"url\"]).nullable(),\n      }),\n      description:\n        'Text input field. Use { \"$bindState\": \"/path\" } for two-way binding.',\n      example: {\n        label: \"Your name\",\n        value: { $bindState: \"/userName\" },\n        placeholder: \"Enter your name\",\n        type: \"text\",\n      },\n    },\n\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z\n          .enum([\"default\", \"secondary\", \"destructive\", \"outline\", \"ghost\"])\n          .nullable(),\n        size: z.enum([\"default\", \"sm\", \"lg\"]).nullable(),\n        disabled: z.boolean().nullable(),\n      }),\n      description:\n        \"Clickable button. Use with on.press to trigger actions like setState.\",\n      example: {\n        label: \"Submit\",\n        variant: \"default\",\n        size: \"default\",\n        disabled: null,\n      },\n    },\n  },\n\n  actions: {},\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Accordion.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Accordion from \"$lib/components/ui/accordion\";\n\n  interface Props extends BaseComponentProps<{\n    items: Array<{ title: string; content: string }>;\n    type?: \"single\" | \"multiple\" | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<Accordion.Root type={props.type === \"single\" ? \"single\" : \"multiple\"} class=\"w-full\">\n  {#each props.items ?? [] as item, i}\n    <Accordion.Item value=\"item-{i}\">\n      <Accordion.Trigger>{item.title}</Accordion.Trigger>\n      <Accordion.Content>\n        <p class=\"text-muted-foreground\">{item.content}</p>\n      </Accordion.Content>\n    </Accordion.Item>\n  {/each}\n</Accordion.Root>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Alert.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Alert from \"$lib/components/ui/alert\";\n\n  interface Props extends BaseComponentProps<{\n    variant?: \"default\" | \"destructive\" | null;\n    title: string;\n    description?: string | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<Alert.Root variant={props.variant ?? \"default\"}>\n  <Alert.Title>{props.title}</Alert.Title>\n  {#if props.description}\n    <Alert.Description>{props.description}</Alert.Description>\n  {/if}\n</Alert.Root>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Badge.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { Badge } from \"$lib/components/ui/badge\";\n\n  interface Props extends BaseComponentProps<{\n    text: string;\n    variant?: \"default\" | \"secondary\" | \"destructive\" | \"outline\" | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<Badge variant={props.variant ?? \"default\"}>{props.text}</Badge>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/BarChart.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string | null;\n    data: Array<Record<string, unknown>>;\n    xKey: string;\n    yKey: string;\n    aggregate?: \"sum\" | \"count\" | \"avg\" | null;\n    color?: string | null;\n    height?: number | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const rawData = $derived(props.data);\n  const rawItems = $derived<Array<Record<string, unknown>>>(\n    Array.isArray(rawData)\n      ? rawData\n      : Array.isArray((rawData as Record<string, unknown>)?.data)\n        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)\n        : []\n  );\n\n  const processedData = $derived(() => {\n    if (rawItems.length === 0) return { items: [], valueKey: props.yKey };\n    \n    const { xKey, yKey, aggregate } = props;\n    \n    if (!aggregate) {\n      const formatted = rawItems.map((item) => ({\n        label: String(item[xKey] ?? \"\"),\n        value: typeof item[yKey] === \"number\" ? item[yKey] : parseFloat(String(item[yKey])) || 0,\n      }));\n      return { items: formatted, valueKey: yKey };\n    }\n\n    const groups = new Map<string, Array<Record<string, unknown>>>();\n    for (const item of rawItems) {\n      const groupKey = String(item[xKey] ?? \"unknown\");\n      const group = groups.get(groupKey) ?? [];\n      group.push(item);\n      groups.set(groupKey, group);\n    }\n\n    const valueKey = aggregate === \"count\" ? \"count\" : yKey;\n    const aggregated: Array<{ label: string; value: number }> = [];\n\n    for (const [key, group] of Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {\n      let value: number;\n      if (aggregate === \"count\") {\n        value = group.length;\n      } else if (aggregate === \"sum\") {\n        value = group.reduce((sum, item) => {\n          const v = item[yKey];\n          return sum + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n        }, 0);\n      } else {\n        const sum = group.reduce((s, item) => {\n          const v = item[yKey];\n          return s + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n        }, 0);\n        value = group.length > 0 ? sum / group.length : 0;\n      }\n      aggregated.push({ label: key, value });\n    }\n\n    return { items: aggregated, valueKey };\n  });\n\n  const chartData = $derived(processedData());\n  const maxValue = $derived(Math.max(...chartData.items.map(d => d.value), 1));\n  const chartColor = $derived(props.color ?? \"var(--chart-1)\");\n</script>\n\n<div class=\"w-full\">\n  {#if props.title}\n    <p class=\"text-sm font-medium mb-2\">{props.title}</p>\n  {/if}\n  \n  {#if chartData.items.length === 0}\n    <div class=\"text-center py-4 text-muted-foreground\">No data available</div>\n  {:else}\n    <div class=\"space-y-2\" style=\"height: {props.height ?? 200}px\">\n      {#each chartData.items as item}\n        <div class=\"flex items-center gap-2\">\n          <span class=\"text-xs text-muted-foreground w-16 truncate\">{item.label}</span>\n          <div class=\"flex-1 h-6 bg-muted rounded overflow-hidden\">\n            <div \n              class=\"h-full rounded transition-all\"\n              style=\"width: {(item.value / maxValue) * 100}%; background-color: {chartColor}\"\n            ></div>\n          </div>\n          <span class=\"text-xs font-medium w-12 text-right\">{item.value.toLocaleString()}</span>\n        </div>\n      {/each}\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Button.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { Button } from \"$lib/components/ui/button\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    variant?: \"default\" | \"secondary\" | \"destructive\" | \"outline\" | \"ghost\" | null;\n    size?: \"default\" | \"sm\" | \"lg\" | null;\n    disabled?: boolean | null;\n  }> {}\n\n  let { props, emit }: Props = $props();\n</script>\n\n<Button\n  variant={props.variant ?? \"default\"}\n  size={props.size ?? \"default\"}\n  disabled={props.disabled ?? false}\n  onclick={() => emit(\"press\")}\n>\n  {props.label}\n</Button>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Callout.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { Info, Lightbulb, AlertTriangle, Star } from \"lucide-svelte\";\n\n  interface Props extends BaseComponentProps<{\n    type?: \"info\" | \"tip\" | \"warning\" | \"important\" | null;\n    title?: string | null;\n    content: string;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const configs = {\n    info: {\n      border: \"border-l-blue-500\",\n      bg: \"bg-blue-500/5\",\n      iconColor: \"text-blue-500\",\n    },\n    tip: {\n      border: \"border-l-emerald-500\",\n      bg: \"bg-emerald-500/5\",\n      iconColor: \"text-emerald-500\",\n    },\n    warning: {\n      border: \"border-l-amber-500\",\n      bg: \"bg-amber-500/5\",\n      iconColor: \"text-amber-500\",\n    },\n    important: {\n      border: \"border-l-purple-500\",\n      bg: \"bg-purple-500/5\",\n      iconColor: \"text-purple-500\",\n    },\n  };\n\n  const config = $derived(configs[props.type ?? \"info\"] ?? configs.info);\n</script>\n\n<div class=\"border-l-4 {config.border} {config.bg} rounded-r-lg p-4\">\n  <div class=\"flex items-start gap-3\">\n    {#if props.type === \"tip\"}\n      <Lightbulb class=\"h-5 w-5 mt-0.5 shrink-0 {config.iconColor}\" />\n    {:else if props.type === \"warning\"}\n      <AlertTriangle class=\"h-5 w-5 mt-0.5 shrink-0 {config.iconColor}\" />\n    {:else if props.type === \"important\"}\n      <Star class=\"h-5 w-5 mt-0.5 shrink-0 {config.iconColor}\" />\n    {:else}\n      <Info class=\"h-5 w-5 mt-0.5 shrink-0 {config.iconColor}\" />\n    {/if}\n    <div class=\"flex-1 min-w-0\">\n      {#if props.title}\n        <p class=\"font-semibold text-sm mb-1\">{props.title}</p>\n      {/if}\n      <p class=\"text-sm text-muted-foreground\">{props.content}</p>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Card.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Card from \"$lib/components/ui/card\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string | null;\n    description?: string | null;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n</script>\n\n<Card.Root>\n  {#if props.title || props.description}\n    <Card.Header>\n      {#if props.title}\n        <Card.Title>{props.title}</Card.Title>\n      {/if}\n      {#if props.description}\n        <Card.Description>{props.description}</Card.Description>\n      {/if}\n    </Card.Header>\n  {/if}\n  <Card.Content class=\"flex flex-col gap-4\">\n    {#if children}\n      {@render children()}\n    {/if}\n  </Card.Content>\n</Card.Root>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Grid.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    columns?: \"1\" | \"2\" | \"3\" | \"4\" | null;\n    gap?: \"sm\" | \"md\" | \"lg\" | null;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n\n  const colsClass = $derived(\n    {\n      \"1\": \"grid-cols-1\",\n      \"2\": \"grid-cols-1 md:grid-cols-2\",\n      \"3\": \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\",\n      \"4\": \"grid-cols-1 md:grid-cols-2 lg:grid-cols-4\",\n    }[props.columns ?? \"3\"] ?? \"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\"\n  );\n  const gapClass = $derived(\n    { sm: \"gap-2\", md: \"gap-4\", lg: \"gap-6\" }[props.gap ?? \"md\"] ?? \"gap-4\"\n  );\n</script>\n\n<div class=\"grid {colsClass} {gapClass}\">\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Heading.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    text: string;\n    level?: \"h1\" | \"h2\" | \"h3\" | \"h4\" | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const level = $derived(props.level ?? \"h2\");\n</script>\n\n{#if level === \"h1\"}\n  <h1 class=\"text-3xl font-bold tracking-tight\">{props.text}</h1>\n{:else if level === \"h2\"}\n  <h2 class=\"text-2xl font-semibold tracking-tight\">{props.text}</h2>\n{:else if level === \"h3\"}\n  <h3 class=\"text-xl font-semibold\">{props.text}</h3>\n{:else}\n  <h4 class=\"text-lg font-medium\">{props.text}</h4>\n{/if}\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/LineChart.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string | null;\n    data: Array<Record<string, unknown>>;\n    xKey: string;\n    yKey: string;\n    aggregate?: \"sum\" | \"count\" | \"avg\" | null;\n    color?: string | null;\n    height?: number | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const rawData = $derived(props.data);\n  const rawItems = $derived<Array<Record<string, unknown>>>(\n    Array.isArray(rawData)\n      ? rawData\n      : Array.isArray((rawData as Record<string, unknown>)?.data)\n        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)\n        : []\n  );\n\n  const processedData = $derived(() => {\n    if (rawItems.length === 0) return [];\n    \n    const { xKey, yKey, aggregate } = props;\n    \n    if (!aggregate) {\n      return rawItems.map((item) => ({\n        label: String(item[xKey] ?? \"\"),\n        value: typeof item[yKey] === \"number\" ? item[yKey] : parseFloat(String(item[yKey])) || 0,\n      }));\n    }\n\n    const groups = new Map<string, Array<Record<string, unknown>>>();\n    for (const item of rawItems) {\n      const groupKey = String(item[xKey] ?? \"unknown\");\n      const group = groups.get(groupKey) ?? [];\n      group.push(item);\n      groups.set(groupKey, group);\n    }\n\n    const aggregated: Array<{ label: string; value: number }> = [];\n    for (const [key, group] of Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {\n      let value: number;\n      if (aggregate === \"count\") {\n        value = group.length;\n      } else if (aggregate === \"sum\") {\n        value = group.reduce((sum, item) => {\n          const v = item[yKey];\n          return sum + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n        }, 0);\n      } else {\n        const sum = group.reduce((s, item) => {\n          const v = item[yKey];\n          return s + (typeof v === \"number\" ? v : parseFloat(String(v)) || 0);\n        }, 0);\n        value = group.length > 0 ? sum / group.length : 0;\n      }\n      aggregated.push({ label: key, value });\n    }\n    return aggregated;\n  });\n\n  const chartData = $derived(processedData());\n  const maxValue = $derived(Math.max(...chartData.map(d => d.value), 1));\n  const minValue = $derived(Math.min(...chartData.map(d => d.value), 0));\n  const chartColor = $derived(props.color ?? \"var(--chart-1)\");\n  const height = $derived(props.height ?? 200);\n  \n  const points = $derived(() => {\n    if (chartData.length === 0) return \"\";\n    const range = maxValue - minValue || 1;\n    return chartData.map((d, i) => {\n      const x = (i / (chartData.length - 1 || 1)) * 100;\n      const y = 100 - ((d.value - minValue) / range) * 100;\n      return `${x},${y}`;\n    }).join(\" \");\n  });\n</script>\n\n<div class=\"w-full\">\n  {#if props.title}\n    <p class=\"text-sm font-medium mb-2\">{props.title}</p>\n  {/if}\n  \n  {#if chartData.length === 0}\n    <div class=\"text-center py-4 text-muted-foreground\">No data available</div>\n  {:else}\n    <div class=\"relative\" style=\"height: {height}px\">\n      <svg viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" class=\"w-full h-full\">\n        <polyline\n          points={points()}\n          fill=\"none\"\n          stroke={chartColor}\n          stroke-width=\"2\"\n          vector-effect=\"non-scaling-stroke\"\n        />\n      </svg>\n      <div class=\"flex justify-between text-xs text-muted-foreground mt-1\">\n        {#each chartData.filter((_, i) => i === 0 || i === chartData.length - 1 || i === Math.floor(chartData.length / 2)) as item}\n          <span>{item.label}</span>\n        {/each}\n      </div>\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Link.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    text: string;\n    href: string;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<a\n  href={props.href}\n  target=\"_blank\"\n  rel=\"noopener noreferrer\"\n  class=\"text-primary underline underline-offset-4 hover:text-primary/80\"\n>\n  {props.text}\n</a>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Metric.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { TrendingUp, TrendingDown, Minus } from \"lucide-svelte\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    value: string;\n    detail?: string | null;\n    trend?: \"up\" | \"down\" | \"neutral\" | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const trendColor = $derived(\n    props.trend === \"up\"\n      ? \"text-green-500\"\n      : props.trend === \"down\"\n        ? \"text-red-500\"\n        : \"text-muted-foreground\"\n  );\n</script>\n\n<div class=\"flex flex-col gap-1\">\n  <p class=\"text-sm text-muted-foreground\">{props.label}</p>\n  <div class=\"flex items-center gap-2\">\n    <span class=\"text-2xl font-bold\">{props.value}</span>\n    {#if props.trend}\n      {#if props.trend === \"up\"}\n        <TrendingUp class=\"h-4 w-4 {trendColor}\" />\n      {:else if props.trend === \"down\"}\n        <TrendingDown class=\"h-4 w-4 {trendColor}\" />\n      {:else}\n        <Minus class=\"h-4 w-4 {trendColor}\" />\n      {/if}\n    {/if}\n  </div>\n  {#if props.detail}\n    <p class=\"text-xs text-muted-foreground\">{props.detail}</p>\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/PieChart.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string | null;\n    data: Array<Record<string, unknown>>;\n    nameKey: string;\n    valueKey: string;\n    height?: number | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const PIE_COLORS = [\n    \"var(--chart-1)\",\n    \"var(--chart-2)\",\n    \"var(--chart-3)\",\n    \"var(--chart-4)\",\n    \"var(--chart-5)\",\n  ];\n\n  const rawData = $derived(props.data);\n  const items = $derived<Array<Record<string, unknown>>>(\n    Array.isArray(rawData)\n      ? rawData\n      : Array.isArray((rawData as Record<string, unknown>)?.data)\n        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)\n        : []\n  );\n\n  const chartData = $derived(\n    items.map((item, i) => ({\n      name: String(item[props.nameKey] ?? `Segment ${i + 1}`),\n      value: typeof item[props.valueKey] === \"number\"\n        ? item[props.valueKey] as number\n        : parseFloat(String(item[props.valueKey])) || 0,\n      color: PIE_COLORS[i % PIE_COLORS.length],\n    }))\n  );\n\n  const total = $derived(chartData.reduce((sum, d) => sum + d.value, 0));\n\n  const segments = $derived(() => {\n    let startAngle = 0;\n    return chartData.map((d) => {\n      const angle = (d.value / (total || 1)) * 360;\n      const segment = {\n        ...d,\n        startAngle,\n        endAngle: startAngle + angle,\n        percentage: Math.round((d.value / (total || 1)) * 100),\n      };\n      startAngle += angle;\n      return segment;\n    });\n  });\n\n  function polarToCartesian(cx: number, cy: number, r: number, angle: number) {\n    const rad = ((angle - 90) * Math.PI) / 180;\n    return {\n      x: cx + r * Math.cos(rad),\n      y: cy + r * Math.sin(rad),\n    };\n  }\n\n  function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {\n    const start = polarToCartesian(cx, cy, r, endAngle);\n    const end = polarToCartesian(cx, cy, r, startAngle);\n    const largeArc = endAngle - startAngle <= 180 ? 0 : 1;\n    return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} 0 ${end.x} ${end.y}`;\n  }\n</script>\n\n<div class=\"w-full\">\n  {#if props.title}\n    <p class=\"text-sm font-medium mb-2\">{props.title}</p>\n  {/if}\n  \n  {#if items.length === 0}\n    <div class=\"text-center py-4 text-muted-foreground\">No data available</div>\n  {:else}\n    <div class=\"flex items-center gap-4\" style=\"height: {props.height ?? 200}px\">\n      <svg viewBox=\"0 0 100 100\" class=\"h-full aspect-square\">\n        {#each segments() as seg}\n          {#if seg.endAngle - seg.startAngle >= 1}\n            <path\n              d={describeArc(50, 50, 35, seg.startAngle, seg.endAngle)}\n              fill=\"none\"\n              stroke={seg.color}\n              stroke-width=\"15\"\n            />\n          {/if}\n        {/each}\n      </svg>\n      <div class=\"flex flex-col gap-1 text-sm\">\n        {#each segments() as seg}\n          <div class=\"flex items-center gap-2\">\n            <span class=\"w-3 h-3 rounded-full\" style=\"background-color: {seg.color}\"></span>\n            <span class=\"text-muted-foreground\">{seg.name}</span>\n            <span class=\"font-medium\">{seg.percentage}%</span>\n          </div>\n        {/each}\n      </div>\n    </div>\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Progress.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { Progress } from \"$lib/components/ui/progress\";\n\n  interface Props extends BaseComponentProps<{\n    value: number;\n    max?: number | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<Progress value={props.value} max={props.max ?? 100} />\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/RadioGroup.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { getBoundProp } from \"@json-render/svelte\";\n  import * as RadioGroup from \"$lib/components/ui/radio-group\";\n  import { Label } from \"$lib/components/ui/label\";\n\n  interface Props extends BaseComponentProps<{\n    label?: string | null;\n    value?: string | null;\n    options: Array<{ value: string; label: string }>;\n  }> {}\n\n  let { props, bindings }: Props = $props();\n\n  function valueBinding() {\n    return getBoundProp<string>(\n      () => (props.value ?? undefined) as string | undefined,\n      () => bindings?.value,\n    );\n  }\n\n  let value = $derived(\n    valueBinding().current ?? \"\"\n  );\n\n  function handleChange(newValue: string) {\n    valueBinding().current = newValue;\n  }\n</script>\n\n<div class=\"flex flex-col gap-2\">\n  {#if props.label}\n    <Label class=\"text-sm font-medium\">{props.label}</Label>\n  {/if}\n  <RadioGroup.Root value={value} onValueChange={handleChange}>\n    {#each props.options ?? [] as opt}\n      <div class=\"flex items-center gap-2\">\n        <RadioGroup.Item value={opt.value} id=\"rg-{opt.value}\" />\n        <Label for=\"rg-{opt.value}\" class=\"font-normal cursor-pointer\">\n          {opt.label}\n        </Label>\n      </div>\n    {/each}\n  </RadioGroup.Root>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/SelectInput.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { getBoundProp } from \"@json-render/svelte\";\n  import * as Select from \"$lib/components/ui/select\";\n  import { Label } from \"$lib/components/ui/label\";\n\n  interface Props extends BaseComponentProps<{\n    label?: string | null;\n    value?: string | null;\n    placeholder?: string | null;\n    options: Array<{ value: string; label: string }>;\n  }> {}\n\n  let { props, bindings }: Props = $props();\n\n  function valueBinding() {\n    return getBoundProp<string>(\n      () => (props.value ?? undefined) as string | undefined,\n      () => bindings?.value,\n    );\n  }\n\n  let value = $derived(\n    valueBinding().current ?? \"\"\n  );\n\n  const selectedOption = $derived(\n    props.options?.find(o => o.value === value)\n  );\n\n  function handleChange(newValue: string | undefined) {\n    if (newValue) {\n      valueBinding().current = newValue;\n    }\n  }\n</script>\n\n<div class=\"flex flex-col gap-2\">\n  {#if props.label}\n    <Label class=\"text-sm font-medium\">{props.label}</Label>\n  {/if}\n  <Select.Root type=\"single\" value={value} onValueChange={handleChange}>\n    <Select.Trigger>\n      {#if selectedOption}\n        {selectedOption.label}\n      {:else}\n        <span class=\"text-muted-foreground\">{props.placeholder ?? \"Select...\"}</span>\n      {/if}\n    </Select.Trigger>\n    <Select.Content>\n      {#each props.options ?? [] as opt}\n        <Select.Item value={opt.value}>{opt.label}</Select.Item>\n      {/each}\n    </Select.Content>\n  </Select.Root>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Separator.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { Separator } from \"$lib/components/ui/separator\";\n\n  interface Props extends BaseComponentProps<Record<string, never>> {}\n\n  let {}: Props = $props();\n</script>\n\n<Separator />\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Skeleton.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    width?: string | null;\n    height?: string | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<div \n  class=\"animate-pulse rounded-md bg-muted {props.width ?? 'w-full'} {props.height ?? 'h-4'}\"\n></div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Stack.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    direction?: \"horizontal\" | \"vertical\" | null;\n    gap?: \"sm\" | \"md\" | \"lg\" | null;\n    wrap?: boolean | null;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n\n  const gapClass = $derived(\n    { sm: \"gap-2\", md: \"gap-4\", lg: \"gap-6\" }[props.gap ?? \"md\"] ?? \"gap-4\"\n  );\n  const dirClass = $derived(\n    props.direction === \"horizontal\" ? \"flex-row\" : \"flex-col\"\n  );\n  const wrapClass = $derived(props.wrap ? \"flex-wrap\" : \"\");\n</script>\n\n<div class=\"flex {dirClass} {wrapClass} {gapClass}\">\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/TabContent.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Tabs from \"$lib/components/ui/tabs\";\n\n  interface Props extends BaseComponentProps<{\n    value: string;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n</script>\n\n<Tabs.Content value={props.value}>\n  {#if children}\n    {@render children()}\n  {/if}\n</Tabs.Content>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Table.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Table from \"$lib/components/ui/table\";\n  import { ArrowUpDown, ArrowUp, ArrowDown } from \"lucide-svelte\";\n\n  interface Props extends BaseComponentProps<{\n    data: Array<Record<string, unknown>>;\n    columns: Array<{ key: string; label: string }>;\n    emptyMessage?: string | null;\n  }> {}\n\n  let { props }: Props = $props();\n\n  let sortKey = $state<string | null>(null);\n  let sortDir = $state<\"asc\" | \"desc\">(\"asc\");\n\n  const rawData = $derived(props.data);\n  const items = $derived<Array<Record<string, unknown>>>(\n    Array.isArray(rawData)\n      ? rawData\n      : Array.isArray((rawData as Record<string, unknown>)?.data)\n        ? ((rawData as Record<string, unknown>).data as Array<Record<string, unknown>>)\n        : []\n  );\n\n  const sorted = $derived(\n    sortKey\n      ? [...items].sort((a, b) => {\n          const av = a[sortKey!];\n          const bv = b[sortKey!];\n          if (typeof av === \"number\" && typeof bv === \"number\") {\n            return sortDir === \"asc\" ? av - bv : bv - av;\n          }\n          const as = String(av ?? \"\");\n          const bs = String(bv ?? \"\");\n          return sortDir === \"asc\" ? as.localeCompare(bs) : bs.localeCompare(as);\n        })\n      : items\n  );\n\n  function handleSort(key: string) {\n    if (sortKey === key) {\n      sortDir = sortDir === \"asc\" ? \"desc\" : \"asc\";\n    } else {\n      sortKey = key;\n      sortDir = \"asc\";\n    }\n  }\n</script>\n\n{#if items.length === 0}\n  <div class=\"text-center py-4 text-muted-foreground\">\n    {props.emptyMessage ?? \"No data\"}\n  </div>\n{:else}\n  <Table.Root>\n    <Table.Header>\n      <Table.Row>\n        {#each props.columns as col}\n          <Table.Head>\n            <button\n              type=\"button\"\n              class=\"inline-flex items-center gap-1 hover:text-foreground transition-colors\"\n              onclick={() => handleSort(col.key)}\n            >\n              {col.label}\n              {#if sortKey === col.key}\n                {#if sortDir === \"asc\"}\n                  <ArrowUp class=\"h-3 w-3 text-muted-foreground\" />\n                {:else}\n                  <ArrowDown class=\"h-3 w-3 text-muted-foreground\" />\n                {/if}\n              {:else}\n                <ArrowUpDown class=\"h-3 w-3 text-muted-foreground\" />\n              {/if}\n            </button>\n          </Table.Head>\n        {/each}\n      </Table.Row>\n    </Table.Header>\n    <Table.Body>\n        {#each sorted as item, i}\n        <Table.Row>\n          {#each props.columns as col}\n            <Table.Cell>{String(item[col.key] ?? \"\")}</Table.Cell>\n          {/each}\n        </Table.Row>\n      {/each}\n    </Table.Body>\n  </Table.Root>\n{/if}\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Tabs.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import * as Tabs from \"$lib/components/ui/tabs\";\n\n  interface Props extends BaseComponentProps<{\n    defaultValue?: string | null;\n    tabs: Array<{ value: string; label: string }>;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n\n  const defaultVal = $derived(\n    props.defaultValue ?? (props.tabs ?? [])[0]?.value ?? \"\"\n  );\n</script>\n\n<Tabs.Root value={defaultVal}>\n  <Tabs.List>\n    {#each props.tabs ?? [] as tab}\n      <Tabs.Trigger value={tab.value}>{tab.label}</Tabs.Trigger>\n    {/each}\n  </Tabs.List>\n  {#if children}\n    {@render children()}\n  {/if}\n</Tabs.Root>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Text.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    content: string;\n    muted?: boolean | null;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<p class={props.muted ? \"text-muted-foreground\" : \"\"}>\n  {props.content}\n</p>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/TextInput.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { getBoundProp } from \"@json-render/svelte\";\n  import { Input } from \"$lib/components/ui/input\";\n  import { Label } from \"$lib/components/ui/label\";\n\n  interface Props extends BaseComponentProps<{\n    label?: string | null;\n    value?: string | null;\n    placeholder?: string | null;\n    type?: \"text\" | \"email\" | \"number\" | \"password\" | \"url\" | null;\n  }> {}\n\n  let { props, bindings }: Props = $props();\n\n  function valueBinding() {\n    return getBoundProp<string>(\n      () => (props.value ?? undefined) as string | undefined,\n      () => bindings?.value,\n    );\n  }\n\n  let value = $derived(\n    valueBinding().current ?? \"\"\n  );\n\n  function handleInput(e: Event) {\n    const newValue = (e.target as HTMLInputElement).value;\n    valueBinding().current = newValue;\n  }\n</script>\n\n<div class=\"flex flex-col gap-2\">\n  {#if props.label}\n    <Label class=\"text-sm font-medium\">{props.label}</Label>\n  {/if}\n  <Input\n    type={props.type ?? \"text\"}\n    placeholder={props.placeholder ?? \"\"}\n    value={value}\n    oninput={handleInput}\n  />\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/components/Timeline.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    items: Array<{\n      title: string;\n      description?: string | null;\n      date?: string | null;\n      status?: \"completed\" | \"current\" | \"upcoming\" | null;\n    }>;\n  }> {}\n\n  let { props }: Props = $props();\n\n  function getDotColor(status: string | null | undefined) {\n    if (status === \"completed\") return \"bg-emerald-500\";\n    if (status === \"current\") return \"bg-blue-500\";\n    return \"bg-muted-foreground/30\";\n  }\n</script>\n\n<div class=\"relative pl-8\">\n  <div class=\"absolute left-[5.5px] top-3 bottom-3 w-px bg-border\"></div>\n  <div class=\"flex flex-col gap-6\">\n    {#each props.items ?? [] as item}\n      <div class=\"relative\">\n        <div class=\"absolute -left-8 top-0.5 h-3 w-3 rounded-full {getDotColor(item.status)} ring-2 ring-background\"></div>\n        <div class=\"flex-1 min-w-0\">\n          <div class=\"flex items-center gap-2 flex-wrap\">\n            <p class=\"font-medium text-sm\">{item.title}</p>\n            {#if item.date}\n              <span class=\"text-xs text-muted-foreground bg-muted px-1.5 py-0.5 rounded\">\n                {item.date}\n              </span>\n            {/if}\n          </div>\n          {#if item.description}\n            <p class=\"text-sm text-muted-foreground mt-1\">{item.description}</p>\n          {/if}\n        </div>\n      </div>\n    {/each}\n  </div>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/render/registry.ts",
    "content": "import { defineRegistry } from \"@json-render/svelte\";\nimport { explorerCatalog } from \"./catalog\";\n\nimport StackComponent from \"./components/Stack.svelte\";\nimport CardComponent from \"./components/Card.svelte\";\nimport GridComponent from \"./components/Grid.svelte\";\nimport HeadingComponent from \"./components/Heading.svelte\";\nimport TextComponent from \"./components/Text.svelte\";\nimport BadgeComponent from \"./components/Badge.svelte\";\nimport AlertComponent from \"./components/Alert.svelte\";\nimport SeparatorComponent from \"./components/Separator.svelte\";\nimport MetricComponent from \"./components/Metric.svelte\";\nimport TableComponent from \"./components/Table.svelte\";\nimport LinkComponent from \"./components/Link.svelte\";\nimport BarChartComponent from \"./components/BarChart.svelte\";\nimport LineChartComponent from \"./components/LineChart.svelte\";\nimport TabsComponent from \"./components/Tabs.svelte\";\nimport TabContentComponent from \"./components/TabContent.svelte\";\nimport ProgressComponent from \"./components/Progress.svelte\";\nimport SkeletonComponent from \"./components/Skeleton.svelte\";\nimport CalloutComponent from \"./components/Callout.svelte\";\nimport AccordionComponent from \"./components/Accordion.svelte\";\nimport TimelineComponent from \"./components/Timeline.svelte\";\nimport PieChartComponent from \"./components/PieChart.svelte\";\nimport RadioGroupComponent from \"./components/RadioGroup.svelte\";\nimport SelectInputComponent from \"./components/SelectInput.svelte\";\nimport TextInputComponent from \"./components/TextInput.svelte\";\nimport ButtonComponent from \"./components/Button.svelte\";\n\nconst components = {\n  Stack: StackComponent,\n  Card: CardComponent,\n  Grid: GridComponent,\n  Heading: HeadingComponent,\n  Text: TextComponent,\n  Badge: BadgeComponent,\n  Alert: AlertComponent,\n  Separator: SeparatorComponent,\n  Metric: MetricComponent,\n  Table: TableComponent,\n  Link: LinkComponent,\n  BarChart: BarChartComponent,\n  LineChart: LineChartComponent,\n  Tabs: TabsComponent,\n  TabContent: TabContentComponent,\n  Progress: ProgressComponent,\n  Skeleton: SkeletonComponent,\n  Callout: CalloutComponent,\n  Accordion: AccordionComponent,\n  Timeline: TimelineComponent,\n  PieChart: PieChartComponent,\n  RadioGroup: RadioGroupComponent,\n  SelectInput: SelectInputComponent,\n  TextInput: TextInputComponent,\n  Button: ButtonComponent,\n};\n\nexport const { registry } = defineRegistry(explorerCatalog, {\n  components,\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/tools/crypto.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction handleFetchError(res: Response, coinId: string) {\n  if (res.status === 404) {\n    return { error: `Cryptocurrency not found: ${coinId}` };\n  }\n  if (res.status === 429) {\n    return { error: \"CoinGecko rate limit exceeded. Try again in a minute.\" };\n  }\n  return { error: `Failed to fetch crypto data: ${res.statusText}` };\n}\n\nfunction sampleTimeSeries(\n  prices: [number, number][],\n  maxPoints: number,\n): Array<{ date: string; price: number }> {\n  const step = Math.max(1, Math.floor(prices.length / maxPoints));\n  return prices\n    .filter((_, i) => i % step === 0)\n    .map(([timestamp, price]) => ({\n      date: new Date(timestamp).toLocaleDateString(\"en-US\", {\n        month: \"short\",\n        day: \"numeric\",\n      }),\n      price: Math.round(price * 100) / 100,\n    }));\n}\n\n// =============================================================================\n// getCryptoPrice — current market data + 7-day sparkline\n// =============================================================================\n\n/**\n * Get cryptocurrency market data from CoinGecko.\n * Free public API, no API key required.\n * https://docs.coingecko.com/reference/introduction\n */\nexport const getCryptoPrice = tool({\n  description:\n    \"Get current price, market cap, 24h change, and 7-day sparkline for a cryptocurrency. For longer price history (30d, 90d, 365d), use getCryptoPriceHistory instead.\",\n  inputSchema: z.object({\n    coinId: z\n      .string()\n      .describe(\n        \"CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana', 'dogecoin', 'cardano')\",\n      ),\n  }),\n  execute: async ({ coinId }) => {\n    const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=true`;\n\n    const res = await fetch(url, {\n      headers: { Accept: \"application/json\" },\n    });\n\n    if (!res.ok) return handleFetchError(res, coinId);\n\n    const data = (await res.json()) as {\n      id: string;\n      symbol: string;\n      name: string;\n      market_data: {\n        current_price: { usd: number };\n        market_cap: { usd: number };\n        total_volume: { usd: number };\n        price_change_percentage_24h: number;\n        price_change_percentage_7d: number;\n        price_change_percentage_30d: number;\n        high_24h: { usd: number };\n        low_24h: { usd: number };\n        ath: { usd: number };\n        ath_date: { usd: string };\n        circulating_supply: number;\n        total_supply: number | null;\n        sparkline_7d: { price: number[] };\n      };\n      market_cap_rank: number;\n    };\n\n    const md = data.market_data;\n\n    // Convert sparkline (hourly array) to dated points\n    const now = Date.now();\n    const sparkline = md.sparkline_7d.price;\n    const step = Math.max(1, Math.floor(sparkline.length / 14));\n    const sparklineData = sparkline\n      .filter((_, i) => i % step === 0)\n      .map((price, i) => {\n        const hourIndex = i * step;\n        const ts = now - (sparkline.length - hourIndex) * 3600_000;\n        return {\n          date: new Date(ts).toLocaleDateString(\"en-US\", {\n            month: \"short\",\n            day: \"numeric\",\n          }),\n          price: Math.round(price * 100) / 100,\n        };\n      });\n\n    return {\n      id: data.id,\n      symbol: data.symbol.toUpperCase(),\n      name: data.name,\n      rank: data.market_cap_rank,\n      price: md.current_price.usd,\n      marketCap: md.market_cap.usd,\n      volume24h: md.total_volume.usd,\n      change24h: Math.round(md.price_change_percentage_24h * 100) / 100,\n      change7d: Math.round(md.price_change_percentage_7d * 100) / 100,\n      change30d: Math.round(md.price_change_percentage_30d * 100) / 100,\n      high24h: md.high_24h.usd,\n      low24h: md.low_24h.usd,\n      allTimeHigh: md.ath.usd,\n      allTimeHighDate: md.ath_date.usd,\n      circulatingSupply: md.circulating_supply,\n      totalSupply: md.total_supply,\n      sparkline7d: sparklineData,\n    };\n  },\n});\n\n// =============================================================================\n// getCryptoPriceHistory — flexible date range price history\n// =============================================================================\n\nexport const getCryptoPriceHistory = tool({\n  description:\n    \"Get historical price data for a cryptocurrency over a specified number of days (e.g., 30, 90, 365). Returns date-labeled data points suitable for charting.\",\n  inputSchema: z.object({\n    coinId: z\n      .string()\n      .describe(\"CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana')\"),\n    days: z\n      .number()\n      .int()\n      .min(1)\n      .max(365)\n      .describe(\"Number of days of history to fetch (e.g., 30, 90, 365)\"),\n  }),\n  execute: async ({ coinId, days }) => {\n    const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}/market_chart?vs_currency=usd&days=${days}`;\n\n    const res = await fetch(url, {\n      headers: { Accept: \"application/json\" },\n    });\n\n    if (!res.ok) return handleFetchError(res, coinId);\n\n    const data = (await res.json()) as {\n      prices: [number, number][];\n    };\n\n    const priceHistory = sampleTimeSeries(data.prices, 20);\n\n    return {\n      coinId,\n      days,\n      priceHistory,\n    };\n  },\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/tools/github.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Shared helpers\n// ---------------------------------------------------------------------------\n\nconst ghHeaders = { Accept: \"application/vnd.github.v3+json\" };\n\nfunction handleGitHubError(res: Response, context: string) {\n  if (res.status === 404) return { error: `Not found: ${context}` };\n  if (res.status === 403)\n    return { error: \"GitHub API rate limit exceeded. Try again later.\" };\n  return { error: `Failed to fetch ${context}: ${res.statusText}` };\n}\n\n// ---------------------------------------------------------------------------\n// getGitHubRepo\n// ---------------------------------------------------------------------------\n\n/**\n * Get public GitHub repository information.\n * Uses the public GitHub REST API (no auth, 60 req/hr rate limit).\n */\nexport const getGitHubRepo = tool({\n  description:\n    \"Get information about a public GitHub repository including stars, forks, open issues, description, language, and recent activity.\",\n  inputSchema: z.object({\n    owner: z.string().describe(\"Repository owner (e.g., 'vercel')\"),\n    repo: z.string().describe(\"Repository name (e.g., 'next.js')\"),\n  }),\n  execute: async ({ owner, repo }) => {\n    const repoUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;\n\n    const [repoRes, languagesRes] = await Promise.all([\n      fetch(repoUrl, { headers: ghHeaders }),\n      fetch(`${repoUrl}/languages`, { headers: ghHeaders }),\n    ]);\n\n    if (!repoRes.ok) {\n      return handleGitHubError(repoRes, `${owner}/${repo}`);\n    }\n\n    const repoData = (await repoRes.json()) as {\n      full_name: string;\n      description: string | null;\n      html_url: string;\n      stargazers_count: number;\n      forks_count: number;\n      open_issues_count: number;\n      watchers_count: number;\n      language: string | null;\n      license: { spdx_id: string } | null;\n      created_at: string;\n      updated_at: string;\n      pushed_at: string;\n      topics: string[];\n      size: number;\n      default_branch: string;\n      archived: boolean;\n      fork: boolean;\n    };\n\n    const languages: Record<string, number> = languagesRes.ok\n      ? ((await languagesRes.json()) as Record<string, number>)\n      : {};\n\n    const totalBytes = Object.values(languages).reduce((a, b) => a + b, 0);\n    const languageBreakdown = Object.entries(languages)\n      .map(([lang, bytes]) => ({\n        language: lang,\n        percentage: Math.round((bytes / totalBytes) * 100),\n        bytes,\n      }))\n      .sort((a, b) => b.bytes - a.bytes)\n      .slice(0, 8);\n\n    return {\n      name: repoData.full_name,\n      description: repoData.description,\n      url: repoData.html_url,\n      stars: repoData.stargazers_count,\n      forks: repoData.forks_count,\n      openIssues: repoData.open_issues_count,\n      watchers: repoData.watchers_count,\n      primaryLanguage: repoData.language,\n      license: repoData.license?.spdx_id ?? \"None\",\n      createdAt: repoData.created_at,\n      updatedAt: repoData.updated_at,\n      lastPush: repoData.pushed_at,\n      topics: repoData.topics,\n      defaultBranch: repoData.default_branch,\n      archived: repoData.archived,\n      isFork: repoData.fork,\n      languages: languageBreakdown,\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// getGitHubPullRequests\n// ---------------------------------------------------------------------------\n\ntype GitHubPR = {\n  number: number;\n  title: string;\n  state: string;\n  html_url: string;\n  user: { login: string } | null;\n  created_at: string;\n  updated_at: string;\n  merged_at: string | null;\n  comments: number;\n  labels: Array<{ name: string }>;\n  draft: boolean;\n};\n\ntype GitHubPRReview = {\n  id: number;\n};\n\ntype GitHubPRReaction = {\n  total_count: number;\n};\n\n/**\n * Get pull requests from a public GitHub repository.\n * Supports filtering by state and sorting by various criteria.\n * Fetches comment counts and reactions for ranking \"most popular\" PRs.\n */\nexport const getGitHubPullRequests = tool({\n  description:\n    \"Get pull requests from a public GitHub repository. Returns titles, authors, state, comment counts, and reactions. Use sort='popularity' to find the most discussed / reacted PRs.\",\n  inputSchema: z.object({\n    owner: z.string().describe(\"Repository owner (e.g., 'vercel')\"),\n    repo: z.string().describe(\"Repository name (e.g., 'next.js')\"),\n    state: z\n      .enum([\"open\", \"closed\", \"all\"])\n      .nullable()\n      .describe(\"Filter by state. Defaults to 'open'.\"),\n    sort: z\n      .enum([\"created\", \"updated\", \"popularity\", \"long-running\"])\n      .nullable()\n      .describe(\n        \"Sort order. 'popularity' sorts by reactions+comments, 'long-running' sorts by age. Defaults to 'created'.\",\n      ),\n    perPage: z\n      .number()\n      .int()\n      .min(1)\n      .max(30)\n      .nullable()\n      .describe(\"Number of PRs to return (1-30). Defaults to 10.\"),\n  }),\n  execute: async ({ owner, repo, state, sort, perPage }) => {\n    const count = perPage ?? 10;\n    const prState = state ?? \"open\";\n\n    // GitHub API sort param: 'popularity' and 'long-running' are API-native\n    const apiSort =\n      sort === \"popularity\"\n        ? \"popularity\"\n        : sort === \"long-running\"\n          ? \"long-running\"\n          : sort === \"updated\"\n            ? \"updated\"\n            : \"created\";\n\n    const url = new URL(\n      `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`,\n    );\n    url.searchParams.set(\"state\", prState);\n    url.searchParams.set(\"sort\", apiSort);\n    url.searchParams.set(\"direction\", \"desc\");\n    url.searchParams.set(\"per_page\", String(count));\n\n    const res = await fetch(url.toString(), { headers: ghHeaders });\n\n    if (!res.ok) {\n      return handleGitHubError(res, `${owner}/${repo} pull requests`);\n    }\n\n    const prs = (await res.json()) as GitHubPR[];\n\n    // Fetch review + reaction counts in parallel for richer data\n    const enriched = await Promise.all(\n      prs.map(async (pr) => {\n        const base = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${pr.number}`;\n\n        const [reviewsRes, reactionsRes] = await Promise.all([\n          fetch(`${base}/reviews?per_page=100`, { headers: ghHeaders }),\n          fetch(\n            `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${pr.number}/reactions`,\n            {\n              headers: {\n                ...ghHeaders,\n                Accept: \"application/vnd.github.squirrel-girl-preview+json\",\n              },\n            },\n          ),\n        ]);\n\n        const reviews: GitHubPRReview[] = reviewsRes.ok\n          ? ((await reviewsRes.json()) as GitHubPRReview[])\n          : [];\n\n        let reactionCount = 0;\n        if (reactionsRes.ok) {\n          const reactions = (await reactionsRes.json()) as GitHubPRReaction[];\n          reactionCount = reactions.length;\n        }\n\n        return {\n          number: pr.number,\n          title: pr.title,\n          state: pr.merged_at ? \"merged\" : pr.state,\n          author: pr.user?.login ?? \"unknown\",\n          url: pr.html_url,\n          createdAt: pr.created_at,\n          updatedAt: pr.updated_at,\n          comments: pr.comments,\n          reviews: reviews.length,\n          reactions: reactionCount,\n          labels: pr.labels.map((l) => l.name),\n          draft: pr.draft,\n        };\n      }),\n    );\n\n    return {\n      repository: `${owner}/${repo}`,\n      state: prState,\n      count: enriched.length,\n      pullRequests: enriched,\n    };\n  },\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/tools/hackernews.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Get top stories from Hacker News.\n * Uses the official HN Firebase API. Free, no auth required.\n * https://github.com/HackerNewsAPI/API\n */\nexport const getHackerNewsTop = tool({\n  description:\n    \"Get the current top stories from Hacker News, including title, score, author, URL, and comment count.\",\n  inputSchema: z.object({\n    count: z\n      .number()\n      .min(1)\n      .max(30)\n      .describe(\"Number of top stories to fetch (1-30)\"),\n  }),\n  execute: async ({ count }) => {\n    const topUrl =\n      \"https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty\";\n    const topRes = await fetch(topUrl);\n\n    if (!topRes.ok) {\n      return { error: \"Failed to fetch Hacker News top stories\" };\n    }\n\n    const topIds = (await topRes.json()) as number[];\n    const storyIds = topIds.slice(0, count);\n\n    const stories = await Promise.all(\n      storyIds.map(async (id) => {\n        const storyRes = await fetch(\n          `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`,\n        );\n        if (!storyRes.ok) return null;\n\n        const story = (await storyRes.json()) as {\n          id: number;\n          title: string;\n          url?: string;\n          score: number;\n          by: string;\n          time: number;\n          descendants?: number;\n          type: string;\n        };\n\n        return {\n          id: story.id,\n          title: story.title,\n          url: story.url ?? `https://news.ycombinator.com/item?id=${story.id}`,\n          score: story.score,\n          author: story.by,\n          comments: story.descendants ?? 0,\n          postedAt: new Date(story.time * 1000).toISOString(),\n          hnUrl: `https://news.ycombinator.com/item?id=${story.id}`,\n        };\n      }),\n    );\n\n    return {\n      stories: stories.filter(Boolean),\n      fetchedAt: new Date().toISOString(),\n    };\n  },\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/tools/search.ts",
    "content": "import { tool, generateText } from \"ai\";\nimport { gateway } from \"@ai-sdk/gateway\";\nimport { z } from \"zod\";\n\n/**\n * Web search tool using Perplexity Sonar via AI Gateway.\n *\n * Perplexity Sonar models have built-in internet access and return\n * synthesized answers with citations. This is wrapped as a regular tool\n * (with an `execute` function) so that ToolLoopAgent can loop: it calls\n * the model, gets results, and feeds them back for the next step.\n */\nexport const webSearch = tool({\n  description:\n    \"Search the web for current information on any topic. Use this when the user asks about something not covered by the specialized tools (weather, crypto, GitHub, Hacker News). Returns a synthesized answer based on real-time web data.\",\n  inputSchema: z.object({\n    query: z\n      .string()\n      .describe(\n        \"The search query — be specific and include relevant context for better results\",\n      ),\n  }),\n  execute: async ({ query }) => {\n    try {\n      const { text } = await generateText({\n        model: gateway(\"perplexity/sonar\"),\n        prompt: query,\n      });\n      return { content: text };\n    } catch (error) {\n      return {\n        error: `Search failed: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      };\n    }\n  },\n});\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/tools/weather.ts",
    "content": "import { tool } from \"ai\";\nimport { z } from \"zod\";\n\n/**\n * Get current weather and 7-day forecast for a city using Open-Meteo API.\n * Free, no API key required.\n * https://open-meteo.com/\n */\nexport const getWeather = tool({\n  description:\n    \"Get current weather conditions and a 7-day forecast for a given city. Returns temperature, humidity, wind speed, weather conditions, and daily forecasts.\",\n  inputSchema: z.object({\n    city: z\n      .string()\n      .describe(\"City name (e.g., 'New York', 'London', 'Tokyo')\"),\n  }),\n  execute: async ({ city }) => {\n    // Step 1: Geocode the city name to coordinates\n    const geocodeUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`;\n    const geocodeRes = await fetch(geocodeUrl);\n\n    if (!geocodeRes.ok) {\n      return { error: `Failed to geocode city: ${city}` };\n    }\n\n    const geocodeData = (await geocodeRes.json()) as {\n      results?: Array<{\n        name: string;\n        country: string;\n        latitude: number;\n        longitude: number;\n        timezone: string;\n      }>;\n    };\n\n    if (!geocodeData.results || geocodeData.results.length === 0) {\n      return { error: `City not found: ${city}` };\n    }\n\n    const location = geocodeData.results[0]!;\n\n    // Step 2: Get weather data\n    const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${location.latitude}&longitude=${location.longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=${encodeURIComponent(location.timezone)}&forecast_days=7`;\n\n    const weatherRes = await fetch(weatherUrl);\n\n    if (!weatherRes.ok) {\n      return { error: \"Failed to fetch weather data\" };\n    }\n\n    const weather = (await weatherRes.json()) as {\n      current: {\n        temperature_2m: number;\n        relative_humidity_2m: number;\n        apparent_temperature: number;\n        weather_code: number;\n        wind_speed_10m: number;\n      };\n      daily: {\n        time: string[];\n        weather_code: number[];\n        temperature_2m_max: number[];\n        temperature_2m_min: number[];\n        precipitation_sum: number[];\n      };\n    };\n\n    const weatherDescription = describeWeatherCode(\n      weather.current.weather_code,\n    );\n\n    const forecast = weather.daily.time.map((date, i) => ({\n      date,\n      day: new Date(date + \"T12:00:00\").toLocaleDateString(\"en-US\", {\n        weekday: \"short\",\n      }),\n      high: Math.round(weather.daily.temperature_2m_max[i]!),\n      low: Math.round(weather.daily.temperature_2m_min[i]!),\n      condition: describeWeatherCode(weather.daily.weather_code[i]!),\n      precipitation: weather.daily.precipitation_sum[i]!,\n    }));\n\n    return {\n      city: location.name,\n      country: location.country,\n      current: {\n        temperature: Math.round(weather.current.temperature_2m),\n        feelsLike: Math.round(weather.current.apparent_temperature),\n        humidity: weather.current.relative_humidity_2m,\n        windSpeed: Math.round(weather.current.wind_speed_10m),\n        condition: weatherDescription,\n      },\n      forecast,\n    };\n  },\n});\n\nfunction describeWeatherCode(code: number): string {\n  const descriptions: Record<number, string> = {\n    0: \"Clear sky\",\n    1: \"Mainly clear\",\n    2: \"Partly cloudy\",\n    3: \"Overcast\",\n    45: \"Foggy\",\n    48: \"Depositing rime fog\",\n    51: \"Light drizzle\",\n    53: \"Moderate drizzle\",\n    55: \"Dense drizzle\",\n    61: \"Slight rain\",\n    63: \"Moderate rain\",\n    65: \"Heavy rain\",\n    71: \"Slight snow\",\n    73: \"Moderate snow\",\n    75: \"Heavy snow\",\n    77: \"Snow grains\",\n    80: \"Slight rain showers\",\n    81: \"Moderate rain showers\",\n    82: \"Violent rain showers\",\n    85: \"Slight snow showers\",\n    86: \"Heavy snow showers\",\n    95: \"Thunderstorm\",\n    96: \"Thunderstorm with slight hail\",\n    99: \"Thunderstorm with heavy hail\",\n  };\n  return descriptions[code] ?? \"Unknown\";\n}\n"
  },
  {
    "path": "examples/svelte-chat/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\nimport type { Snippet } from \"svelte\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n// Utility types for shadcn-svelte components\nexport type WithElementRef<T, E extends HTMLElement = HTMLElement> = T & {\n  ref?: E | null;\n};\n\n// WithoutChild should only omit \"child\" (singular), not \"children\"\n// children is the Svelte 5 snippet pattern, which is still used\nexport type WithoutChild<T> = Omit<T, \"child\"> & { children?: Snippet };\n\nexport type WithoutChildrenOrChild<T> = Omit<T, \"children\" | \"child\">;\n"
  },
  {
    "path": "examples/svelte-chat/src/routes/+layout.svelte",
    "content": "<script lang=\"ts\">\n  import \"../app.css\";\n  import favicon from \"$lib/assets/favicon.svg\";\n\n  let { children } = $props();\n</script>\n\n<svelte:head>\n  <link rel=\"icon\" href={favicon} />\n  <title>json-render Svelte Chat</title>\n</svelte:head>\n\n{@render children()}\n"
  },
  {
    "path": "examples/svelte-chat/src/routes/+page.svelte",
    "content": "<script lang=\"ts\">\n  import { Chat } from \"@ai-sdk/svelte\";\n  import { DefaultChatTransport } from \"ai\";\n  import { SPEC_DATA_PART_TYPE } from \"@json-render/core\";\n  import {\n    buildSpecFromParts,\n    getTextFromParts,\n    type DataPart,\n  } from \"@json-render/svelte\";\n  import type { Spec } from \"@json-render/svelte\";\n  import ExplorerRenderer from \"$lib/render/Renderer.svelte\";\n  import { ArrowDown, ArrowUp, Loader2, Sparkles } from \"lucide-svelte\";\n\n  // =============================================================================\n  // Chat Setup\n  // =============================================================================\n\n  let input = $state(\"\");\n  let showScrollButton = $state(false);\n  let isStickToBottom = $state(true);\n  let scrollContainer: HTMLElement | null = null;\n  let inputRef: HTMLTextAreaElement | null = null;\n\n  const chat = new Chat({\n    transport: new DefaultChatTransport({\n      api: \"/api/generate\",\n    }),\n  });\n\n  const isStreaming = $derived(\n    chat.status === \"streaming\" || chat.status === \"submitted\",\n  );\n  const isEmpty = $derived(chat.messages.length === 0);\n\n  // =============================================================================\n  // Suggestions\n  // =============================================================================\n\n  const SUGGESTIONS = [\n    {\n      label: \"Weather comparison\",\n      prompt: \"Compare the weather in New York, London, and Tokyo\",\n    },\n    {\n      label: \"GitHub repo stats\",\n      prompt: \"Show me stats for the vercel/next.js and vercel/ai GitHub repos\",\n    },\n    {\n      label: \"Crypto dashboard\",\n      prompt: \"Build a crypto dashboard for Bitcoin, Ethereum, and Solana\",\n    },\n    {\n      label: \"Hacker News top stories\",\n      prompt: \"Show me the top 15 Hacker News stories right now\",\n    },\n  ];\n\n  // =============================================================================\n  // Tool Labels\n  // =============================================================================\n\n  const TOOL_LABELS: Record<string, [string, string]> = {\n    getWeather: [\"Getting weather data\", \"Got weather data\"],\n    getGitHubRepo: [\"Fetching GitHub repo\", \"Fetched GitHub repo\"],\n    getGitHubPullRequests: [\"Fetching pull requests\", \"Fetched pull requests\"],\n    getCryptoPrice: [\"Looking up crypto price\", \"Looked up crypto price\"],\n    getCryptoPriceHistory: [\"Fetching price history\", \"Fetched price history\"],\n    getHackerNewsTop: [\"Loading Hacker News\", \"Loaded Hacker News\"],\n    webSearch: [\"Searching the web\", \"Searched the web\"],\n  };\n\n  // =============================================================================\n  // Message Handling\n  // =============================================================================\n\n  function handleSubmit(text?: string) {\n    const message = text || input;\n    if (!message.trim() || isStreaming) return;\n    input = \"\";\n    chat.sendMessage({ text: message.trim() });\n  }\n\n  function handleKeyDown(e: KeyboardEvent) {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      handleSubmit();\n    }\n  }\n\n  function handleClear() {\n    chat.messages = [];\n    input = \"\";\n    inputRef?.focus();\n  }\n\n  function scrollToBottom() {\n    if (!scrollContainer) return;\n    isStickToBottom = true;\n    showScrollButton = false;\n    scrollContainer.scrollTo({\n      top: scrollContainer.scrollHeight,\n      behavior: \"smooth\",\n    });\n  }\n\n  // =============================================================================\n  // Scroll tracking\n  // =============================================================================\n\n  function handleScroll() {\n    if (!scrollContainer) return;\n    const { scrollTop, scrollHeight, clientHeight } = scrollContainer;\n    const atBottom = scrollTop + clientHeight >= scrollHeight - 80;\n    isStickToBottom = atBottom;\n    showScrollButton = !atBottom;\n  }\n\n  // Auto-scroll on new messages\n  $effect(() => {\n    if (scrollContainer && isStickToBottom && chat.messages.length > 0) {\n      scrollContainer.scrollTop = scrollContainer.scrollHeight;\n    }\n  });\n\n  // =============================================================================\n  // Helpers\n  // =============================================================================\n\n  function getSpec(parts: DataPart[]): Spec | null {\n    return buildSpecFromParts(parts);\n  }\n\n  function getText(parts: DataPart[]): string {\n    return getTextFromParts(parts);\n  }\n\n  function hasSpec(parts: DataPart[]): boolean {\n    return parts.some((p) => p.type === SPEC_DATA_PART_TYPE);\n  }\n\n  interface ToolInfo {\n    toolCallId: string;\n    toolName: string;\n    state: string;\n    output?: unknown;\n  }\n\n  type Segment =\n    | { kind: \"text\"; text: string }\n    | { kind: \"tools\"; tools: ToolInfo[] }\n    | { kind: \"spec\" };\n\n  function getSegments(parts: DataPart[]): {\n    segments: Segment[];\n    specInserted: boolean;\n  } {\n    const segments: Segment[] = [];\n    let specInserted = false;\n\n    for (const part of parts) {\n      if (part.type === \"text\" && part.text) {\n        const text = part.text;\n        if (!text.trim()) continue;\n        const last = segments[segments.length - 1];\n        if (last?.kind === \"text\") {\n          last.text += text;\n        } else {\n          segments.push({ kind: \"text\", text });\n        }\n      } else if (part.type.startsWith(\"tool-\")) {\n        const tp = part as {\n          type: string;\n          toolCallId?: string;\n          state?: string;\n          output?: unknown;\n        };\n        const last = segments[segments.length - 1];\n        const toolInfo: ToolInfo = {\n          toolCallId: tp.toolCallId || \"\",\n          toolName: tp.type.replace(/^tool-/, \"\"),\n          state: tp.state || \"\",\n          output: tp.output,\n        };\n        if (last?.kind === \"tools\") {\n          last.tools.push(toolInfo);\n        } else {\n          segments.push({ kind: \"tools\", tools: [toolInfo] });\n        }\n      } else if (part.type === SPEC_DATA_PART_TYPE && !specInserted) {\n        segments.push({ kind: \"spec\" });\n        specInserted = true;\n      }\n    }\n\n    return { segments, specInserted };\n  }\n</script>\n\n<div class=\"h-screen flex flex-col overflow-hidden\">\n  <!-- Header -->\n  <header\n    class=\"border-b px-6 py-3 flex items-center justify-between flex-shrink-0\">\n    <div class=\"flex items-center gap-3\">\n      <h1 class=\"text-lg font-semibold\">json-render Svelte Chat</h1>\n    </div>\n    <div class=\"flex items-center gap-2\">\n      {#if chat.messages.length > 0}\n        <button\n          onclick={handleClear}\n          class=\"px-3 py-1.5 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors\">\n          Start Over\n        </button>\n      {/if}\n    </div>\n  </header>\n\n  <!-- Messages area -->\n  <main\n    bind:this={scrollContainer}\n    onscroll={handleScroll}\n    class=\"flex-1 overflow-auto\">\n    {#if isEmpty}\n      <!-- Empty state -->\n      <div class=\"h-full flex flex-col items-center justify-center px-6 py-12\">\n        <div class=\"max-w-2xl w-full space-y-8\">\n          <div class=\"text-center space-y-2\">\n            <h2 class=\"text-2xl font-semibold tracking-tight\">\n              What would you like to explore?\n            </h2>\n            <p class=\"text-muted-foreground\">\n              Ask about weather, GitHub repos, crypto prices, or Hacker News --\n              the agent will fetch real data and build a dashboard.\n            </p>\n          </div>\n\n          <!-- Suggestions -->\n          <div class=\"flex flex-wrap gap-2 justify-center\">\n            {#each SUGGESTIONS as s}\n              <button\n                onclick={() => handleSubmit(s.prompt)}\n                class=\"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors\">\n                <Sparkles class=\"h-3 w-3\" />\n                {s.label}\n              </button>\n            {/each}\n          </div>\n        </div>\n      </div>\n    {:else}\n      <!-- Message thread -->\n      <div class=\"max-w-4xl mx-auto px-10 py-6 space-y-6\">\n        {#each chat.messages as message, index}\n          {@const isLast = index === chat.messages.length - 1}\n          {@const parts = message.parts as DataPart[]}\n          {@const spec = getSpec(parts)}\n          {@const text = getText(parts)}\n          {@const messageHasSpec = hasSpec(parts)}\n          {@const { segments, specInserted } = getSegments(parts)}\n\n          {#if message.role === \"user\"}\n            <!-- User message -->\n            <div class=\"flex justify-end\">\n              {#if text}\n                <div\n                  class=\"max-w-[85%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap bg-primary text-primary-foreground rounded-tr-md\">\n                  {text}\n                </div>\n              {/if}\n            </div>\n          {:else}\n            <!-- Assistant message -->\n            {@const hasAnything = segments.length > 0 || messageHasSpec}\n            {@const showLoader = isLast && isStreaming && !hasAnything}\n            {@const showSpecAtEnd = messageHasSpec && !specInserted}\n\n            <div class=\"w-full flex flex-col gap-3\">\n              {#each segments as seg, i}\n                {#if seg.kind === \"text\"}\n                  <div\n                    class=\"text-sm leading-relaxed [&_p+p]:mt-3 [&_ul]:mt-2 [&_ol]:mt-2 [&_pre]:mt-2\">\n                    {seg.text}\n                  </div>\n                {:else if seg.kind === \"spec\"}\n                  {#if spec}\n                    <div class=\"w-full\">\n                      <ExplorerRenderer\n                        {spec}\n                        loading={isLast && isStreaming} />\n                    </div>\n                  {/if}\n                {:else if seg.kind === \"tools\"}\n                  <div class=\"flex flex-col gap-1\">\n                    {#each seg.tools as t}\n                      {@const toolIsLoading =\n                        t.state !== \"output-available\" &&\n                        t.state !== \"output-error\" &&\n                        t.state !== \"output-denied\"}\n                      {@const labels = TOOL_LABELS[t.toolName]}\n                      {@const label = labels\n                        ? toolIsLoading\n                          ? labels[0]\n                          : labels[1]\n                        : t.toolName}\n\n                      <div class=\"text-sm group\">\n                        <span\n                          class=\"text-muted-foreground {toolIsLoading\n                            ? 'animate-shimmer'\n                            : ''}\">\n                          {label}\n                        </span>\n                      </div>\n                    {/each}\n                  </div>\n                {/if}\n              {/each}\n\n              <!-- Loading indicator -->\n              {#if showLoader}\n                <div class=\"text-sm text-muted-foreground animate-shimmer\">\n                  Thinking...\n                </div>\n              {/if}\n\n              <!-- Fallback: render spec at end if no inline position was found -->\n              {#if showSpecAtEnd && spec}\n                <div class=\"w-full\">\n                  <ExplorerRenderer {spec} loading={isLast && isStreaming} />\n                </div>\n              {/if}\n            </div>\n          {/if}\n        {/each}\n\n        <!-- Error display -->\n        {#if chat.error}\n          <div\n            class=\"rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive\">\n            {chat.error.message}\n          </div>\n        {/if}\n      </div>\n    {/if}\n  </main>\n\n  <!-- Input bar -->\n  <div class=\"px-6 pb-3 flex-shrink-0 bg-background relative\">\n    <!-- Scroll to bottom button -->\n    {#if showScrollButton && !isEmpty}\n      <button\n        onclick={scrollToBottom}\n        class=\"absolute left-1/2 -translate-x-1/2 -top-10 z-10 h-8 w-8 rounded-full border border-border bg-background text-muted-foreground shadow-md flex items-center justify-center hover:text-foreground hover:bg-accent transition-colors\"\n        aria-label=\"Scroll to bottom\">\n        <ArrowDown class=\"h-4 w-4\" />\n      </button>\n    {/if}\n\n    <div class=\"max-w-4xl mx-auto relative\">\n      <textarea\n        bind:this={inputRef}\n        bind:value={input}\n        onkeydown={handleKeyDown}\n        placeholder={isEmpty\n          ? \"e.g., Compare weather in NYC, London, and Tokyo...\"\n          : \"Ask a follow-up...\"}\n        rows={2}\n        class=\"w-full resize-none rounded-xl border border-input bg-card px-4 py-3 pr-12 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\"\n      ></textarea>\n      <button\n        onclick={() => handleSubmit()}\n        disabled={!input.trim() || isStreaming}\n        class=\"absolute right-3 bottom-3 h-8 w-8 rounded-lg bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\">\n        {#if isStreaming}\n          <Loader2 class=\"h-4 w-4 animate-spin\" />\n        {:else}\n          <ArrowUp class=\"h-4 w-4\" />\n        {/if}\n      </button>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "examples/svelte-chat/src/routes/api/generate/+server.ts",
    "content": "import { agent } from \"$lib/agent\";\nimport { minuteRateLimit, dailyRateLimit } from \"$lib/rate-limit\";\nimport {\n  convertToModelMessages,\n  createUIMessageStream,\n  createUIMessageStreamResponse,\n  type UIMessage,\n} from \"ai\";\nimport { pipeJsonRender } from \"@json-render/core\";\nimport type { RequestHandler } from \"./$types\";\n\nexport const POST: RequestHandler = async ({ request }) => {\n  const ip =\n    request.headers.get(\"x-forwarded-for\")?.split(\",\")[0] ?? \"anonymous\";\n\n  const [minuteResult, dailyResult] = await Promise.all([\n    minuteRateLimit.limit(ip),\n    dailyRateLimit.limit(ip),\n  ]);\n\n  if (!minuteResult.success || !dailyResult.success) {\n    const isMinuteLimit = !minuteResult.success;\n    return new Response(\n      JSON.stringify({\n        error: \"Rate limit exceeded\",\n        message: isMinuteLimit\n          ? \"Too many requests. Please wait a moment before trying again.\"\n          : \"Daily limit reached. Please try again tomorrow.\",\n      }),\n      {\n        status: 429,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const body = await request.json();\n  const uiMessages: UIMessage[] = body.messages;\n\n  if (!uiMessages || !Array.isArray(uiMessages) || uiMessages.length === 0) {\n    return new Response(\n      JSON.stringify({ error: \"messages array is required\" }),\n      {\n        status: 400,\n        headers: { \"Content-Type\": \"application/json\" },\n      },\n    );\n  }\n\n  const modelMessages = await convertToModelMessages(uiMessages);\n  const result = await agent.stream({ messages: modelMessages });\n\n  const stream = createUIMessageStream({\n    execute: async ({ writer }) => {\n      writer.merge(pipeJsonRender(result.toUIMessageStream()));\n    },\n  });\n\n  return createUIMessageStreamResponse({ stream });\n};\n"
  },
  {
    "path": "examples/svelte-chat/static/robots.txt",
    "content": "# allow crawling everything by default\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "examples/svelte-chat/svelte.config.js",
    "content": "import adapter from \"@sveltejs/adapter-auto\";\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n  kit: {\n    adapter: adapter(),\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "examples/svelte-chat/tsconfig.json",
    "content": "{\n\t\"extends\": \"./.svelte-kit/tsconfig.json\",\n\t\"compilerOptions\": {\n\t\t\"rewriteRelativeImportExtensions\": true,\n\t\t\"allowJs\": true,\n\t\t\"checkJs\": true,\n\t\t\"esModuleInterop\": true,\n\t\t\"forceConsistentCasingInFileNames\": true,\n\t\t\"resolveJsonModule\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"sourceMap\": true,\n\t\t\"strict\": true,\n\t\t\"moduleResolution\": \"bundler\"\n\t}\n\t// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias\n\t// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files\n\t//\n\t// To make changes to top-level options such as include and exclude, we recommend extending\n\t// the generated config; see https://svelte.dev/docs/kit/configuration#typescript\n}\n"
  },
  {
    "path": "examples/svelte-chat/vite.config.ts",
    "content": "import { sveltekit } from \"@sveltejs/kit/vite\";\nimport tailwindcss from \"@tailwindcss/vite\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  plugins: [sveltekit(), tailwindcss()],\n  optimizeDeps: {\n    exclude: [\"@json-render/svelte\"],\n  },\n});\n"
  },
  {
    "path": "examples/vite-renderers/CHANGELOG.md",
    "content": "# vite-renderers\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n  - @json-render/solid@0.14.1\n  - @json-render/svelte@0.14.1\n  - @json-render/vue@0.14.1\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n  - @json-render/solid@0.14.0\n  - @json-render/svelte@0.14.0\n  - @json-render/vue@0.14.0\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/solid@0.13.0\n  - @json-render/react@0.13.0\n  - @json-render/svelte@0.13.0\n  - @json-render/vue@0.13.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n  - @json-render/svelte@0.12.1\n  - @json-render/vue@0.12.1\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/svelte@0.12.0\n  - @json-render/react@0.12.0\n  - @json-render/vue@0.12.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n  - @json-render/vue@0.11.0\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n  - @json-render/vue@0.10.0\n"
  },
  {
    "path": "examples/vite-renderers/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>@json-render multi-renderer demo</title>\n    <meta property=\"og:title\" content=\"Multi-Renderer Example | json-render\" />\n    <meta property=\"og:image\" content=\"/og-image.png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"Multi-Renderer Example | json-render\" />\n    <meta name=\"twitter:image\" content=\"/og-image.png\" />\n    <style>\n      * {\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n\n      body {\n        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n        background: #f3f4f6;\n        min-height: 100vh;\n      }\n\n      #renderer-root {\n        max-width: 640px;\n        margin: 0 auto;\n        padding: 24px;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"renderer-root\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/vite-renderers/package.json",
    "content": "{\n  \"name\": \"vite-renderers\",\n  \"version\": \"0.1.7\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless vite-renderers.json-render vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/solid\": \"workspace:*\",\n    \"@json-render/svelte\": \"workspace:*\",\n    \"@json-render/vue\": \"workspace:*\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"solid-js\": \"^1.9.11\",\n    \"svelte\": \"^5.49.2\",\n    \"vue\": \"^3.5.29\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.4\",\n    \"@vitejs/plugin-vue\": \"^6.0.4\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\",\n    \"vite-plugin-solid\": \"^2.11.10\"\n  }\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/main.ts",
    "content": "import \"./shared/styles.css\";\nimport { demoSpec } from \"./spec\";\n\ntype Renderer = \"vue\" | \"react\" | \"svelte\" | \"solid\";\n\nconst container = document.getElementById(\"renderer-root\") as HTMLElement;\n\nlet unmountCurrent: (() => void) | null = null;\n\nasync function switchTo(renderer: Renderer) {\n  unmountCurrent?.();\n  container.innerHTML = \"\";\n  if (renderer === \"vue\") {\n    const mod = await import(\"./vue/mount.ts\");\n    mod.mount(container, renderer, demoSpec);\n    unmountCurrent = mod.unmount;\n  } else if (renderer === \"react\") {\n    const mod = await import(\"./react/mount.tsx\");\n    mod.mount(container, renderer, demoSpec);\n    unmountCurrent = mod.unmount;\n  } else if (renderer === \"svelte\") {\n    const mod = await import(\"./svelte/mount.ts\");\n    mod.mount(container, renderer, demoSpec);\n    unmountCurrent = mod.unmount;\n  } else {\n    const mod = await import(\"./solid/mount.tsx\");\n    mod.mount(container, renderer, demoSpec);\n    unmountCurrent = mod.unmount;\n  }\n}\n\n// The RendererTabs component (rendered by JSON renderer) dispatches this event\ndocument.addEventListener(\"switch-renderer\", (e: Event) => {\n  switchTo((e as CustomEvent<string>).detail as Renderer);\n});\n\n// Default: Vue\nswitchTo(\"vue\");\n"
  },
  {
    "path": "examples/vite-renderers/src/react/App.tsx",
    "content": "import { useMemo } from \"react\";\nimport type { Spec } from \"@json-render/core\";\nimport {\n  StateProvider,\n  ActionProvider,\n  VisibilityProvider,\n  ValidationProvider,\n  Renderer,\n  defineRegistry,\n  useStateStore,\n} from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\nimport { components } from \"./registry\";\nimport { actionStubs, makeHandlers } from \"../shared/handlers\";\n\nconst { registry } = defineRegistry(catalog, {\n  components,\n  actions: actionStubs,\n});\n\nfunction DemoRenderer({ spec }: { spec: Spec }) {\n  const { get, set } = useStateStore();\n  const handlers = useMemo(() => makeHandlers(get, set), [get, set]);\n  return (\n    <ActionProvider handlers={handlers}>\n      <VisibilityProvider>\n        <ValidationProvider>\n          <Renderer spec={spec} registry={registry} />\n        </ValidationProvider>\n      </VisibilityProvider>\n    </ActionProvider>\n  );\n}\n\nexport default function App({\n  initialRenderer = \"vue\",\n  spec,\n}: {\n  initialRenderer?: string;\n  spec: Spec;\n}) {\n  return (\n    <div className={`renderer-${initialRenderer}`}>\n      <StateProvider\n        initialState={{ ...spec.state, renderer: initialRenderer }}\n      >\n        <DemoRenderer spec={spec} />\n      </StateProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/react/catalog.ts",
    "content": "import { schema } from \"@json-render/react/schema\";\nimport { catalogDef } from \"../shared/catalog-def\";\n\nexport const catalog = schema.createCatalog(catalogDef);\nexport type AppCatalog = typeof catalog;\n"
  },
  {
    "path": "examples/vite-renderers/src/react/mount.tsx",
    "content": "import { createRoot, type Root } from \"react-dom/client\";\nimport type { Spec } from \"@json-render/core\";\nimport App from \"./App\";\n\nlet root: Root | null = null;\n\nexport function mount(container: HTMLElement, renderer: string, spec: Spec) {\n  root = createRoot(container);\n  root.render(<App initialRenderer={renderer} spec={spec} />);\n}\n\nexport function unmount() {\n  root?.unmount();\n  root = null;\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/react/registry.tsx",
    "content": "import type { Components } from \"@json-render/react\";\nimport type { AppCatalog } from \"./catalog\";\n\nexport const components: Components<AppCatalog> = {\n  Stack: ({ props, children }) => (\n    <div\n      className={[\n        \"json-render-stack\",\n        props.direction === \"horizontal\" && \"json-render-stack--horizontal\",\n        props.align && `json-render-stack--align-${props.align}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      style={{\n        gap: props.gap ? `${props.gap}px` : undefined,\n        padding: props.padding ? `${props.padding}px` : undefined,\n      }}\n    >\n      {children}\n    </div>\n  ),\n\n  Card: ({ props, children }) => (\n    <div className=\"json-render-card\">\n      {props.title && (\n        <div className=\"json-render-card-title-wrap\">\n          <h2 className=\"json-render-card-title\">{props.title}</h2>\n        </div>\n      )}\n      {props.subtitle && (\n        <p className=\"json-render-card-subtitle\">{props.subtitle}</p>\n      )}\n      {children}\n    </div>\n  ),\n\n  Text: ({ props }) => (\n    <span\n      className={[\n        \"json-render-text\",\n        props.size && props.size !== \"md\" && `json-render-text--${props.size}`,\n        props.weight &&\n          props.weight !== \"normal\" &&\n          `json-render-text--${props.weight}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      style={props.color ? { color: props.color } : undefined}\n    >\n      {String(props.content ?? \"\")}\n    </span>\n  ),\n\n  Button: ({ props, emit }) => (\n    <button\n      disabled={props.disabled}\n      onClick={() => emit(\"press\")}\n      className={[\n        \"json-render-button\",\n        props.variant && `json-render-button--${props.variant}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      {props.label}\n    </button>\n  ),\n\n  Badge: ({ props }) => (\n    <span\n      className=\"json-render-badge\"\n      style={\n        props.color\n          ? {\n              backgroundColor: `${props.color}20`,\n              color: props.color,\n              borderColor: `${props.color}40`,\n            }\n          : undefined\n      }\n    >\n      {props.label}\n    </span>\n  ),\n\n  ListItem: ({ props, emit }) => (\n    <div\n      onClick={() => emit(\"press\")}\n      className={[\n        \"json-render-list-item\",\n        props.completed && \"json-render-list-item--done\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <div\n        className={[\n          \"json-render-list-item-check\",\n          props.completed && \"json-render-list-item-check--done\",\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {props.completed ? \"✓\" : \"\"}\n      </div>\n      <span\n        className={[\n          \"json-render-list-item-text\",\n          props.completed && \"json-render-list-item-text--done\",\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {props.title}\n      </span>\n    </div>\n  ),\n\n  RendererBadge: ({ props }) => (\n    <span className=\"json-render-renderer-badge\">\n      <span className=\"json-render-renderer-dot\" />\n      {props.renderer === \"vue\"\n        ? \"Rendered with Vue\"\n        : props.renderer === \"react\"\n          ? \"Rendered with React\"\n          : props.renderer === \"svelte\"\n            ? \"Rendered with Svelte\"\n            : \"Rendered with Solid\"}\n    </span>\n  ),\n\n  RendererTabs: ({ props, emit }) => (\n    <div className=\"json-render-renderer-tabs-wrapper\">\n      <span className=\"json-render-renderer-tabs-label\">Render</span>\n      <div className=\"json-render-renderer-tabs\">\n        <button\n          onClick={() => emit(\"pressVue\")}\n          className={[\n            \"json-render-renderer-tab\",\n            props.renderer === \"vue\" && \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Vue\n        </button>\n        <button\n          onClick={() => emit(\"pressReact\")}\n          className={[\n            \"json-render-renderer-tab\",\n            props.renderer === \"react\" && \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          React\n        </button>\n        <button\n          onClick={() => emit(\"pressSvelte\")}\n          className={[\n            \"json-render-renderer-tab\",\n            props.renderer === \"svelte\" && \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Svelte\n        </button>\n        <button\n          onClick={() => emit(\"pressSolid\")}\n          className={[\n            \"json-render-renderer-tab\",\n            props.renderer === \"solid\" && \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Solid\n        </button>\n      </div>\n    </div>\n  ),\n};\n"
  },
  {
    "path": "examples/vite-renderers/src/shared/catalog-def.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Shared catalog definition — imported by both vue/catalog.ts and react/catalog.ts.\n * Each renderer calls schema.createCatalog(catalogDef) with its own schema instance.\n */\nexport const catalogDef = {\n  components: {\n    Stack: {\n      props: z.object({\n        gap: z.number().optional(),\n        padding: z.number().optional(),\n        direction: z.enum([\"vertical\", \"horizontal\"]).optional(),\n        align: z.enum([\"start\", \"center\", \"end\"]).optional(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Layout container that stacks children vertically or horizontally\",\n    },\n    Card: {\n      props: z.object({\n        title: z.string().optional(),\n        subtitle: z.string().optional(),\n      }),\n      slots: [\"default\"],\n      description: \"A card container with optional title and subtitle\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).optional(),\n        weight: z.enum([\"normal\", \"medium\", \"bold\"]).optional(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"Displays a text string\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).optional(),\n        disabled: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A clickable button that emits a 'press' event\",\n    },\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A small badge/tag label\",\n    },\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().optional(),\n        completed: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A single item in a list\",\n    },\n    RendererTabs: {\n      props: z.object({ renderer: z.string() }),\n      slots: [],\n      description:\n        \"Segmented tab control for switching between Vue, React, Svelte, and Solid renderers\",\n    },\n    RendererBadge: {\n      props: z.object({ renderer: z.string() }),\n      slots: [],\n      description: \"Badge indicating which renderer is currently active\",\n    },\n  },\n  actions: {\n    increment: {\n      params: z.object({}),\n      description: \"Increment the counter by 1\",\n    },\n    decrement: {\n      params: z.object({}),\n      description: \"Decrement the counter by 1\",\n    },\n    reset: { params: z.object({}), description: \"Reset the counter to 0\" },\n    toggleItem: {\n      params: z.object({ index: z.number() }),\n      description: \"Toggle the completed state of a todo item\",\n    },\n    switchToVue: {\n      params: z.object({}),\n      description: \"Switch to the Vue renderer\",\n    },\n    switchToReact: {\n      params: z.object({}),\n      description: \"Switch to the React renderer\",\n    },\n    switchToSvelte: {\n      params: z.object({}),\n      description: \"Switch to the Svelte renderer\",\n    },\n    switchToSolid: {\n      params: z.object({}),\n      description: \"Switch to the Solid renderer\",\n    },\n  },\n};\n"
  },
  {
    "path": "examples/vite-renderers/src/shared/handlers.ts",
    "content": "type Get = (path: string) => unknown;\ntype Set = (path: string, value: unknown) => void;\n\n/** Stub actions for defineRegistry (no-ops; real logic is in makeHandlers) */\nexport const actionStubs = {\n  increment: async () => {},\n  decrement: async () => {},\n  reset: async () => {},\n  toggleItem: async () => {},\n  switchToVue: async () => {},\n  switchToReact: async () => {},\n  switchToSvelte: async () => {},\n  switchToSolid: async () => {},\n};\n\n/** Creates action handlers that close over the state store's get/set */\nexport function makeHandlers(get: Get, set: Set) {\n  return {\n    increment: async () => {\n      set(\"/count\", Number(get(\"/count\") || 0) + 1);\n    },\n    decrement: async () => {\n      set(\"/count\", Math.max(0, Number(get(\"/count\") || 0) - 1));\n    },\n    reset: async () => {\n      set(\"/count\", 0);\n    },\n    toggleItem: async (params: Record<string, unknown>) => {\n      const index = params.index as number;\n      const todos = (\n        get(\"/todos\") as Array<{\n          id: number;\n          title: string;\n          completed: boolean;\n        }>\n      ).slice();\n      const item = todos[index];\n      if (item) todos[index] = { ...item, completed: !item.completed };\n      set(\"/todos\", todos);\n    },\n    switchToVue: async () => {\n      document.dispatchEvent(\n        new CustomEvent(\"switch-renderer\", { detail: \"vue\" }),\n      );\n    },\n    switchToReact: async () => {\n      document.dispatchEvent(\n        new CustomEvent(\"switch-renderer\", { detail: \"react\" }),\n      );\n    },\n    switchToSvelte: async () => {\n      document.dispatchEvent(\n        new CustomEvent(\"switch-renderer\", { detail: \"svelte\" }),\n      );\n    },\n    switchToSolid: async () => {\n      document.dispatchEvent(\n        new CustomEvent(\"switch-renderer\", { detail: \"solid\" }),\n      );\n    },\n  };\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/shared/styles.css",
    "content": "/* ---- Stack ---------------------------------------------------------------- */\n\n.json-render-stack {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n}\n\n.json-render-stack--horizontal {\n  flex-direction: row;\n  align-items: center;\n}\n\n.json-render-stack--align-start {\n  align-items: flex-start;\n}\n.json-render-stack--align-center {\n  align-items: center;\n}\n.json-render-stack--align-end {\n  align-items: flex-end;\n}\n\n/* ---- Card ----------------------------------------------------------------- */\n\n.json-render-card {\n  background-color: white;\n  border-radius: 12px;\n  border: 1px solid #e5e7eb;\n  padding: 20px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n\n.json-render-card-title-wrap {\n  margin-bottom: 4px;\n}\n\n.json-render-card-title {\n  font-size: 16px;\n  font-weight: 600;\n  color: #111827;\n  margin: 0;\n}\n\n.json-render-card-subtitle {\n  font-size: 13px;\n  color: #6b7280;\n  margin: 0 0 12px 0;\n}\n\n/* ---- Text ----------------------------------------------------------------- */\n\n.json-render-text {\n  font-size: 14px;\n  font-weight: 400;\n  color: #111827;\n}\n\n.json-render-text--sm {\n  font-size: 12px;\n}\n.json-render-text--lg {\n  font-size: 16px;\n}\n.json-render-text--xl {\n  font-size: 24px;\n}\n\n.json-render-text--medium {\n  font-weight: 500;\n}\n.json-render-text--bold {\n  font-weight: 700;\n}\n\n/* ---- Button --------------------------------------------------------------- */\n\n.json-render-button {\n  padding: 8px 16px;\n  border-radius: 8px;\n  border: none;\n  cursor: pointer;\n  font-weight: 500;\n  font-size: 14px;\n  transition: background 0.15s;\n  background-color: #3b82f6;\n  color: white;\n}\n\n.json-render-button:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.json-render-button--primary {\n  background-color: #3b82f6;\n  color: white;\n}\n\n.json-render-button--secondary {\n  background-color: #f3f4f6;\n  color: #374151;\n}\n\n.json-render-button--danger {\n  background-color: #fee2e2;\n  color: #dc2626;\n}\n\n/* ---- Badge ---------------------------------------------------------------- */\n\n.json-render-badge {\n  display: inline-block;\n  padding: 4px 12px;\n  border-radius: 999px;\n  font-size: 13px;\n  font-weight: 500;\n  background-color: #e0f2fe;\n  color: #0369a1;\n  border: 1px solid #bae6fd;\n}\n\n/* ---- ListItem ------------------------------------------------------------- */\n\n.json-render-list-item {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  padding: 10px 12px;\n  border-radius: 8px;\n  cursor: pointer;\n  background-color: #f9fafb;\n  border: 1px solid #e5e7eb;\n}\n\n.json-render-list-item--done {\n  background-color: #f0fdf4;\n  border-color: #bbf7d0;\n}\n\n.json-render-list-item-check {\n  width: 18px;\n  height: 18px;\n  border-radius: 50%;\n  border: 2px solid #d1d5db;\n  background-color: transparent;\n  flex-shrink: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 11px;\n  color: white;\n}\n\n.json-render-list-item-check--done {\n  border-color: #16a34a;\n  background-color: #16a34a;\n}\n\n.json-render-list-item-text {\n  font-size: 14px;\n  color: #111827;\n  text-decoration: none;\n}\n\n.json-render-list-item-text--done {\n  color: #6b7280;\n  text-decoration: line-through;\n}\n\n/* ---- RendererBadge -------------------------------------------------------- */\n\n.json-render-renderer-badge {\n  display: inline-flex;\n  align-items: center;\n  gap: 5px;\n  padding: 10px 15px;\n  border-radius: 999px;\n  font-size: 12px;\n  font-weight: 500;\n  /* default colors; overridden by renderer parent class below */\n  background-color: #e0f2fe;\n  color: #0369a1;\n  border: 1px solid #bae6fd;\n}\n\n.json-render-renderer-dot {\n  width: 6px;\n  height: 6px;\n  border-radius: 50%;\n  display: inline-block;\n  /* background overridden by renderer parent class below */\n  background-color: #0369a1;\n}\n\n/* ---- RendererTabs --------------------------------------------------------- */\n\n.json-render-renderer-tabs-wrapper {\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  margin-left: auto;\n}\n\n.json-render-renderer-tabs-label {\n  font-size: 13px;\n  color: #6b7280;\n  font-weight: 500;\n}\n\n.json-render-renderer-tabs {\n  display: inline-flex;\n  border-radius: 8px;\n  border: 1px solid #e5e7eb;\n  overflow: hidden;\n}\n\n.json-render-renderer-tab {\n  padding: 6px 16px;\n  border: none;\n  border-right: 0;\n  cursor: pointer;\n  font-size: 13px;\n  font-weight: 500;\n  background-color: white;\n  color: #374151;\n  transition: background 0.15s;\n}\n\n.json-render-renderer-tab:not(:last-child) {\n  border-right: 1px solid #e5e7eb;\n}\n\n/* ---- Renderer-specific overrides ----------------------------------------- */\n\n.renderer-vue .json-render-renderer-badge {\n  color: #42b883;\n  background-color: #42b88318;\n  border-color: #42b88340;\n}\n\n.renderer-vue .json-render-renderer-dot {\n  background-color: #42b883;\n}\n\n.renderer-vue .json-render-renderer-tab--active {\n  background-color: #42b883;\n  color: white;\n}\n\n.renderer-react .json-render-renderer-badge {\n  color: #149eca;\n  background-color: #149eca18;\n  border-color: #149eca40;\n}\n\n.renderer-react .json-render-renderer-dot {\n  background-color: #149eca;\n}\n\n.renderer-react .json-render-renderer-tab--active {\n  background-color: #149eca;\n  color: white;\n}\n\n.renderer-svelte .json-render-renderer-badge {\n  color: #ff3e00;\n  background-color: #ff3e0018;\n  border-color: #ff3e0040;\n}\n\n.renderer-svelte .json-render-renderer-dot {\n  background-color: #ff3e00;\n}\n\n.renderer-svelte .json-render-renderer-tab--active {\n  background-color: #ff3e00;\n  color: white;\n}\n\n.renderer-solid .json-render-renderer-badge {\n  color: #2c4f7c;\n  background-color: #2c4f7c18;\n  border-color: #2c4f7c40;\n}\n\n.renderer-solid .json-render-renderer-dot {\n  background-color: #2c4f7c;\n}\n\n.renderer-solid .json-render-renderer-tab--active {\n  background-color: #2c4f7c;\n  color: white;\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/solid/App.tsx",
    "content": "import type { Spec } from \"@json-render/core\";\nimport { StateProvider } from \"@json-render/solid\";\nimport { DemoRenderer } from \"./DemoRenderer\";\n\nexport function App(props: { initialRenderer?: string; spec: Spec }) {\n  const renderer = props.initialRenderer ?? \"solid\";\n  const initialState = {\n    ...props.spec.state,\n    renderer,\n  };\n\n  return (\n    <div class={`renderer-${renderer}`}>\n      <StateProvider initialState={initialState}>\n        <DemoRenderer spec={props.spec} />\n      </StateProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/solid/DemoRenderer.tsx",
    "content": "import type { Spec } from \"@json-render/core\";\nimport {\n  ActionProvider,\n  ValidationProvider,\n  VisibilityProvider,\n  Renderer,\n  defineRegistry,\n  useStateStore,\n} from \"@json-render/solid\";\nimport { catalog } from \"./catalog\";\nimport { components } from \"./registry\";\nimport { actionStubs, makeHandlers } from \"../shared/handlers\";\n\nconst { registry } = defineRegistry(catalog, {\n  components,\n  actions: actionStubs,\n});\n\nexport function DemoRenderer(props: { spec: Spec }) {\n  const stateStore = useStateStore();\n  const handlers = makeHandlers(stateStore.get, stateStore.set);\n\n  return (\n    <ActionProvider handlers={handlers}>\n      <VisibilityProvider>\n        <ValidationProvider>\n          <Renderer spec={props.spec} registry={registry} />\n        </ValidationProvider>\n      </VisibilityProvider>\n    </ActionProvider>\n  );\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/solid/catalog.ts",
    "content": "import { schema } from \"@json-render/solid/schema\";\nimport { catalogDef } from \"../shared/catalog-def\";\n\nexport const catalog = schema.createCatalog(catalogDef);\nexport type AppCatalog = typeof catalog;\n"
  },
  {
    "path": "examples/vite-renderers/src/solid/mount.tsx",
    "content": "import { render } from \"solid-js/web\";\nimport type { Spec } from \"@json-render/core\";\nimport { App } from \"./App\";\n\nlet dispose: (() => void) | null = null;\n\nexport function mount(container: HTMLElement, renderer: string, spec: Spec) {\n  dispose = render(\n    () => <App initialRenderer={renderer} spec={spec} />,\n    container,\n  );\n}\n\nexport function unmount() {\n  dispose?.();\n  dispose = null;\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/solid/registry.tsx",
    "content": "import type { Components } from \"@json-render/solid\";\nimport type { AppCatalog } from \"./catalog\";\n\nexport const components: Components<AppCatalog> = {\n  Stack: (renderProps) => (\n    <div\n      class={[\n        \"json-render-stack\",\n        renderProps.props.direction === \"horizontal\" &&\n          \"json-render-stack--horizontal\",\n        renderProps.props.align &&\n          `json-render-stack--align-${renderProps.props.align}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      style={{\n        gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,\n        padding: renderProps.props.padding\n          ? `${renderProps.props.padding}px`\n          : undefined,\n      }}\n    >\n      {renderProps.children}\n    </div>\n  ),\n\n  Card: (renderProps) => (\n    <div class=\"json-render-card\">\n      {renderProps.props.title && (\n        <div class=\"json-render-card-title-wrap\">\n          <h2 class=\"json-render-card-title\">{renderProps.props.title}</h2>\n        </div>\n      )}\n      {renderProps.props.subtitle && (\n        <p class=\"json-render-card-subtitle\">{renderProps.props.subtitle}</p>\n      )}\n      {renderProps.children}\n    </div>\n  ),\n\n  Text: (renderProps) => (\n    <span\n      class={[\n        \"json-render-text\",\n        renderProps.props.size !== \"md\" &&\n          renderProps.props.size &&\n          `json-render-text--${renderProps.props.size}`,\n        renderProps.props.weight !== \"normal\" &&\n          renderProps.props.weight &&\n          `json-render-text--${renderProps.props.weight}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n      style={renderProps.props.color ? { color: renderProps.props.color } : {}}\n    >\n      {String(renderProps.props.content ?? \"\")}\n    </span>\n  ),\n\n  Button: (renderProps) => (\n    <button\n      disabled={renderProps.props.disabled}\n      onClick={() => renderProps.emit(\"press\")}\n      class={[\n        \"json-render-button\",\n        renderProps.props.variant &&\n          `json-render-button--${renderProps.props.variant}`,\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      {renderProps.props.label}\n    </button>\n  ),\n\n  Badge: (renderProps) => (\n    <span\n      class=\"json-render-badge\"\n      style={\n        renderProps.props.color\n          ? {\n              \"background-color\": `${renderProps.props.color}20`,\n              color: renderProps.props.color,\n              \"border-color\": `${renderProps.props.color}40`,\n            }\n          : {}\n      }\n    >\n      {renderProps.props.label}\n    </span>\n  ),\n\n  ListItem: (renderProps) => (\n    <div\n      onClick={() => renderProps.emit(\"press\")}\n      class={[\n        \"json-render-list-item\",\n        renderProps.props.completed && \"json-render-list-item--done\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}\n    >\n      <div\n        class={[\n          \"json-render-list-item-check\",\n          renderProps.props.completed && \"json-render-list-item-check--done\",\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {renderProps.props.completed ? \"✓\" : \"\"}\n      </div>\n      <span\n        class={[\n          \"json-render-list-item-text\",\n          renderProps.props.completed && \"json-render-list-item-text--done\",\n        ]\n          .filter(Boolean)\n          .join(\" \")}\n      >\n        {renderProps.props.title}\n      </span>\n    </div>\n  ),\n\n  RendererBadge: (renderProps) => (\n    <span class=\"json-render-renderer-badge\">\n      <span class=\"json-render-renderer-dot\" />\n      {renderProps.props.renderer === \"vue\"\n        ? \"Rendered with Vue\"\n        : renderProps.props.renderer === \"react\"\n          ? \"Rendered with React\"\n          : renderProps.props.renderer === \"svelte\"\n            ? \"Rendered with Svelte\"\n            : \"Rendered with Solid\"}\n    </span>\n  ),\n\n  RendererTabs: (renderProps) => (\n    <div class=\"json-render-renderer-tabs-wrapper\">\n      <span class=\"json-render-renderer-tabs-label\">Render</span>\n      <div class=\"json-render-renderer-tabs\">\n        <button\n          onClick={() => renderProps.emit(\"pressVue\")}\n          class={[\n            \"json-render-renderer-tab\",\n            renderProps.props.renderer === \"vue\" &&\n              \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Vue\n        </button>\n        <button\n          onClick={() => renderProps.emit(\"pressReact\")}\n          class={[\n            \"json-render-renderer-tab\",\n            renderProps.props.renderer === \"react\" &&\n              \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          React\n        </button>\n        <button\n          onClick={() => renderProps.emit(\"pressSvelte\")}\n          class={[\n            \"json-render-renderer-tab\",\n            renderProps.props.renderer === \"svelte\" &&\n              \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Svelte\n        </button>\n        <button\n          onClick={() => renderProps.emit(\"pressSolid\")}\n          class={[\n            \"json-render-renderer-tab\",\n            renderProps.props.renderer === \"solid\" &&\n              \"json-render-renderer-tab--active\",\n          ]\n            .filter(Boolean)\n            .join(\" \")}\n        >\n          Solid\n        </button>\n      </div>\n    </div>\n  ),\n};\n"
  },
  {
    "path": "examples/vite-renderers/src/spec.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport const demoSpec: Spec = {\n  root: \"root\",\n  state: {\n    renderer: \"vue\",\n    count: 0,\n    todos: [\n      { id: 1, title: \"Learn JSON Render\", completed: true },\n      {\n        id: 2,\n        title:\n          \"Try @json-render/vue, @json-render/react, @json-render/svelte, and @json-render/solid\",\n        completed: false,\n      },\n      { id: 3, title: \"Build something awesome\", completed: false },\n    ],\n  },\n  elements: {\n    root: {\n      type: \"Stack\",\n      props: { gap: 24, padding: 24, direction: \"vertical\" },\n      children: [\n        \"demo-title\",\n        \"renderer-tabs\",\n        \"renderer-badge\",\n        \"counter-card\",\n        \"milestone-badge\",\n        \"todos-card\",\n      ],\n    },\n\n    \"demo-title\": {\n      type: \"Text\",\n      props: {\n        content: \"@json-render multi-renderer demo\",\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"renderer-badge\": {\n      type: \"RendererBadge\",\n      props: { renderer: { $state: \"/renderer\" } },\n    },\n    \"renderer-tabs\": {\n      type: \"RendererTabs\",\n      props: { renderer: { $state: \"/renderer\" } },\n      on: {\n        pressVue: { action: \"switchToVue\" },\n        pressReact: { action: \"switchToReact\" },\n        pressSvelte: { action: \"switchToSvelte\" },\n        pressSolid: { action: \"switchToSolid\" },\n      },\n    },\n\n    // ---- Counter card ----\n    \"counter-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Counter\",\n        subtitle: \"Click the buttons to change the count\",\n      },\n      children: [\"counter-body\"],\n    },\n    \"counter-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"horizontal\", align: \"center\" },\n      children: [\n        \"decrement-btn\",\n        \"counter-value\",\n        \"increment-btn\",\n        \"reset-btn\",\n      ],\n    },\n    \"decrement-btn\": {\n      type: \"Button\",\n      props: { label: \"−\", variant: \"secondary\" },\n      on: { press: { action: \"decrement\" } },\n    },\n    \"counter-value\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/count\" },\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"increment-btn\": {\n      type: \"Button\",\n      props: { label: \"+\", variant: \"primary\" },\n      on: { press: { action: \"increment\" } },\n    },\n    \"reset-btn\": {\n      type: \"Button\",\n      props: { label: \"Reset\", variant: \"danger\" },\n      on: { press: { action: \"reset\" } },\n    },\n\n    // ---- Milestone badge (visible only when count >= 10) ----\n    \"milestone-badge\": {\n      type: \"Badge\",\n      props: { label: \"Milestone reached: 10!\", color: \"#10b981\" },\n      visible: { $state: \"/count\", gte: 10 },\n    },\n\n    // ---- Todos card ----\n    \"todos-card\": {\n      type: \"Card\",\n      props: { title: \"Todo List\", subtitle: \"Your tasks\" },\n      children: [\"todos-list\"],\n    },\n    \"todos-list\": {\n      type: \"Stack\",\n      props: { gap: 8, direction: \"vertical\" },\n      repeat: { statePath: \"/todos\", key: \"id\" },\n      children: [\"todo-item\"],\n    },\n    \"todo-item\": {\n      type: \"ListItem\",\n      props: {\n        title: { $item: \"title\" },\n        completed: { $item: \"completed\" },\n      },\n      on: {\n        press: { action: \"toggleItem\", params: { index: { $index: true } } },\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/App.svelte",
    "content": "<script lang=\"ts\">\n  import type { Spec } from \"@json-render/core\";\n  import { StateProvider } from \"@json-render/svelte\";\n  import DemoRenderer from \"./DemoRenderer.svelte\";\n\n  interface Props {\n    initialRenderer?: string;\n    spec: Spec;\n  }\n\n  let { initialRenderer = \"svelte\", spec }: Props = $props();\n  let initialState = $derived({\n    ...spec.state,\n    renderer: initialRenderer,\n  });\n</script>\n\n<div class={\"renderer-\" + initialRenderer}>\n  <StateProvider {initialState}>\n    <DemoRenderer {spec} />\n  </StateProvider>\n</div>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/DemoRenderer.svelte",
    "content": "<script lang=\"ts\">\n  import {\n    ActionProvider,\n    ValidationProvider,\n    VisibilityProvider,\n    Renderer,\n    getStateContext,\n  } from \"@json-render/svelte\";\n  import type { Spec } from \"@json-render/core\";\n  import { registry } from \"./registry\";\n  import { makeHandlers } from \"../shared/handlers\";\n\n  interface Props {\n    spec: Spec;\n  }\n\n  let { spec }: Props = $props();\n  const state = getStateContext();\n  const handlers = makeHandlers(state.get, state.set);\n</script>\n\n<ActionProvider {handlers}>\n  <VisibilityProvider>\n    <ValidationProvider>\n      <Renderer {spec} {registry} />\n    </ValidationProvider>\n  </VisibilityProvider>\n</ActionProvider>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/catalog.ts",
    "content": "import { schema } from \"@json-render/svelte/schema\";\nimport { catalogDef } from \"../shared/catalog-def\";\n\nexport const catalog = schema.createCatalog(catalogDef);\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Badge.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    color?: string;\n  }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<span\n  style=\"\n    display: inline-block;\n    padding: 4px 12px;\n    border-radius: 999px;\n    font-size: 13px;\n    font-weight: 500;\n    background-color: {props.color ? `${props.color}20` : '#e0f2fe'};\n    color: {props.color ?? '#0369a1'};\n    border: 1px solid {props.color ? `${props.color}40` : '#bae6fd'};\n  \">\n  {props.label}\n</span>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Button.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    label: string;\n    variant?: \"primary\" | \"secondary\" | \"danger\";\n    disabled?: boolean;\n  }> {}\n\n  let { props, emit }: Props = $props();\n\n  let backgroundColor = $derived(\n    props.variant === \"danger\"\n      ? \"#fee2e2\"\n      : props.variant === \"secondary\"\n        ? \"#f3f4f6\"\n        : \"#3b82f6\",\n  );\n  let textColor = $derived(\n    props.variant === \"danger\"\n      ? \"#dc2626\"\n      : props.variant === \"secondary\"\n        ? \"#374151\"\n        : \"white\",\n  );\n</script>\n\n<button\n  disabled={props.disabled}\n  onclick={() => emit(\"press\")}\n  style=\"\n    padding: 8px 16px;\n    border-radius: 8px;\n    border: none;\n    cursor: {props.disabled ? 'not-allowed' : 'pointer'};\n    font-weight: 500;\n    font-size: 14px;\n    transition: background 0.15s;\n    opacity: {props.disabled ? '0.5' : '1'};\n    background-color: {backgroundColor};\n    color: {textColor};\n  \">\n  {props.label}\n</button>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Card.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title?: string;\n    subtitle?: string;\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n</script>\n\n<div\n  style=\"\n    background: white;\n    border-radius: 12px;\n    border: 1px solid #e5e7eb;\n    padding: 20px;\n    box-shadow: 0 1px 3px rgba(0,0,0,0.05);\n  \">\n  {#if props.title}\n    <h2\n      style=\"\n        font-size: 16px;\n        font-weight: 600;\n        color: #111827;\n        margin: 0 0 4px 0;\n      \">\n      {props.title}\n    </h2>\n  {/if}\n  {#if props.subtitle}\n    <p\n      style=\"\n        font-size: 13px;\n        color: #6b7280;\n        margin: 0 0 12px 0;\n      \">\n      {props.subtitle}\n    </p>\n  {/if}\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Input.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n  import { getBoundProp } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    value?: string;\n    placeholder?: string;\n  }> {}\n\n  let { props, bindings }: Props = $props();\n\n  let value = getBoundProp<string>(\n    () => props.value,\n    () => bindings?.value,\n  );\n</script>\n\n<input\n  bind:value={value.current}\n  placeholder={props.placeholder ?? \"\"}\n  style=\"\n    padding: 8px 12px;\n    border-radius: 8px;\n    border: 1px solid #d1d5db;\n    font-size: 14px;\n    outline: none;\n    width: 100%;\n    box-sizing: border-box;\n  \" />\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/ListItem.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    title: string;\n    completed?: boolean;\n  }> {}\n\n  let { props, emit }: Props = $props();\n</script>\n\n<button\n  type=\"button\"\n  onclick={() => emit(\"press\")}\n  style=\"\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    padding: 10px 12px;\n    border-radius: 8px;\n    cursor: pointer;\n    background-color: {props.completed ? '#f0fdf4' : '#f9fafb'};\n    border: 1px solid {props.completed ? '#bbf7d0' : '#e5e7eb'};\n  \">\n  <div\n    style=\"\n      width: 18px;\n      height: 18px;\n      border-radius: 50%;\n      border: 2px solid {props.completed ? '#16a34a' : '#d1d5db'};\n      background-color: {props.completed ? '#16a34a' : 'transparent'};\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      color: white;\n      font-size: 11px;\n      flex-shrink: 0;\n    \">\n    {props.completed ? \"✓\" : \"\"}\n  </div>\n  <span\n    style=\"\n      font-size: 14px;\n      color: {props.completed ? '#6b7280' : '#111827'};\n      text-decoration: {props.completed ? 'line-through' : 'none'};\n    \">\n    {props.title}\n  </span>\n</button>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/RendererBadge.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ renderer: string }> {}\n\n  let { props }: Props = $props();\n</script>\n\n<span class=\"json-render-renderer-badge\">\n  <span class=\"json-render-renderer-dot\"></span>\n  {props.renderer === \"vue\"\n    ? \"Rendered with Vue\"\n    : props.renderer === \"react\"\n      ? \"Rendered with React\"\n      : props.renderer === \"svelte\"\n        ? \"Rendered with Svelte\"\n        : \"Rendered with Solid\"}\n</span>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/RendererTabs.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ renderer: string }> {}\n\n  let { props, emit }: Props = $props();\n</script>\n\n<div class=\"json-render-renderer-tabs-wrapper\">\n  <span class=\"json-render-renderer-tabs-label\">Render</span>\n  <div class=\"json-render-renderer-tabs\">\n    <button\n      type=\"button\"\n      onclick={() => emit(\"pressVue\")}\n      class={[\n        \"json-render-renderer-tab\",\n        props.renderer === \"vue\" && \"json-render-renderer-tab--active\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}>\n      Vue\n    </button>\n    <button\n      type=\"button\"\n      onclick={() => emit(\"pressReact\")}\n      class={[\n        \"json-render-renderer-tab\",\n        props.renderer === \"react\" && \"json-render-renderer-tab--active\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}>\n      React\n    </button>\n    <button\n      type=\"button\"\n      onclick={() => emit(\"pressSvelte\")}\n      class={[\n        \"json-render-renderer-tab\",\n        props.renderer === \"svelte\" && \"json-render-renderer-tab--active\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}>\n      Svelte\n    </button>\n    <button\n      type=\"button\"\n      onclick={() => emit(\"pressSolid\")}\n      class={[\n        \"json-render-renderer-tab\",\n        props.renderer === \"solid\" && \"json-render-renderer-tab--active\",\n      ]\n        .filter(Boolean)\n        .join(\" \")}>\n      Solid\n    </button>\n  </div>\n</div>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Stack.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    gap?: number;\n    padding?: number;\n    direction?: \"vertical\" | \"horizontal\";\n    align?: \"start\" | \"center\" | \"end\";\n  }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n  let horizontal = $derived(props.direction === \"horizontal\");\n</script>\n\n<div\n  style=\"\n    display: flex;\n    flex-direction: {horizontal ? 'row' : 'column'};\n    gap: {props.gap ?? 0}px;\n    padding: {props.padding ?? 0}px;\n    align-items: {props.align === 'start'\n    ? 'flex-start'\n    : props.align === 'end'\n      ? 'flex-end'\n      : horizontal\n        ? 'center'\n        : 'stretch'};\n  \">\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/components/Text.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{\n    content: string;\n    size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n    weight?: \"normal\" | \"medium\" | \"bold\";\n    color?: string;\n  }> {}\n\n  let { props }: Props = $props();\n\n  const sizeMap: Record<string, string> = {\n    sm: \"12px\",\n    md: \"14px\",\n    lg: \"16px\",\n    xl: \"24px\",\n  };\n\n  const weightMap: Record<string, string> = {\n    normal: \"400\",\n    medium: \"500\",\n    bold: \"700\",\n  };\n</script>\n\n<span\n  style=\"\n    font-size: {sizeMap[props.size ?? 'md'] ?? '14px'};\n    font-weight: {weightMap[props.weight ?? 'normal'] ?? '400'};\n    color: {props.color ?? '#111827'};\n  \">\n  {String(props.content ?? \"\")}\n</span>\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/mount.ts",
    "content": "import { mount as mountComponent, unmount as unmountComponent } from \"svelte\";\nimport type { Spec } from \"@json-render/core\";\nimport App from \"./App.svelte\";\n\nlet app: ReturnType<typeof mountComponent> | null = null;\n\nexport function mount(container: HTMLElement, renderer: string, spec: Spec) {\n  app = mountComponent(App, {\n    target: container,\n    props: { initialRenderer: renderer, spec },\n  });\n}\n\nexport function unmount() {\n  if (app) {\n    unmountComponent(app);\n    app = null;\n  }\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/svelte/registry.ts",
    "content": "import { defineRegistry, type ComponentRegistry } from \"@json-render/svelte\";\nimport { catalog } from \"./catalog\";\nimport { actionStubs } from \"../shared/handlers\";\n\nimport Stack from \"./components/Stack.svelte\";\nimport Card from \"./components/Card.svelte\";\nimport Text from \"./components/Text.svelte\";\nimport Button from \"./components/Button.svelte\";\nimport Badge from \"./components/Badge.svelte\";\nimport ListItem from \"./components/ListItem.svelte\";\nimport RendererBadge from \"./components/RendererBadge.svelte\";\nimport RendererTabs from \"./components/RendererTabs.svelte\";\n\nconst components: ComponentRegistry = {\n  Stack,\n  Card,\n  Text,\n  Button,\n  Badge,\n  ListItem,\n  RendererBadge,\n  RendererTabs,\n};\n\nexport const { registry } = defineRegistry(catalog, {\n  components,\n  actions: actionStubs,\n});\n"
  },
  {
    "path": "examples/vite-renderers/src/vue/App.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Spec } from \"@json-render/core\";\nimport { StateProvider } from \"@json-render/vue\";\nimport DemoRenderer from \"./DemoRenderer.vue\";\n\nconst props = defineProps<{\n  initialRenderer?: string; spec: Spec }>();\nconst initialState = {\n  ...props.spec.state,\n  renderer: props.initialRenderer ?? \"vue\" };\n</script>\n\n<template>\n  <div :class=\"`renderer-${props.initialRenderer ?? 'vue'}`\">\n    <StateProvider :initial-state=\"initialState\">\n      <DemoRenderer :spec=\"props.spec\" />\n    </StateProvider>\n  </div>\n</template>\n"
  },
  {
    "path": "examples/vite-renderers/src/vue/DemoRenderer.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Spec } from \"@json-render/core\";\nimport {\n  ActionProvider, ValidationProvider, VisibilityProvider,\n  Renderer, defineRegistry, useStateStore,\n} from \"@json-render/vue\";\nimport { catalog } from \"./catalog\";\nimport { components } from \"./registry\";\nimport { actionStubs, makeHandlers } from \"../shared/handlers\";\n\nconst props = defineProps<{ spec: Spec }>();\nconst { get, set } = useStateStore();\nconst { registry } = defineRegistry(catalog, { components, actions: actionStubs });\nconst handlers = makeHandlers(get, set);\n</script>\n\n<template>\n  <ActionProvider :handlers=\"handlers\">\n    <VisibilityProvider>\n      <ValidationProvider>\n        <Renderer :spec=\"props.spec\" :registry=\"registry\" />\n      </ValidationProvider>\n    </VisibilityProvider>\n  </ActionProvider>\n</template>\n"
  },
  {
    "path": "examples/vite-renderers/src/vue/catalog.ts",
    "content": "import { schema } from \"@json-render/vue/schema\";\nimport { catalogDef } from \"../shared/catalog-def\";\n\nexport const catalog = schema.createCatalog(catalogDef);\nexport type AppCatalog = typeof catalog;\n"
  },
  {
    "path": "examples/vite-renderers/src/vue/mount.ts",
    "content": "import { createApp, type App } from \"vue\";\nimport type { Spec } from \"@json-render/core\";\nimport VueApp from \"./App.vue\";\n\nlet app: App | null = null;\n\nexport function mount(container: HTMLElement, renderer: string, spec: Spec) {\n  app = createApp(VueApp, { initialRenderer: renderer, spec });\n  app.mount(container);\n}\n\nexport function unmount() {\n  app?.unmount();\n  app = null;\n}\n"
  },
  {
    "path": "examples/vite-renderers/src/vue/registry.ts",
    "content": "import { h } from \"vue\";\nimport type { Components } from \"@json-render/vue\";\nimport type { AppCatalog } from \"./catalog\";\n\nexport const components: Components<AppCatalog> = {\n  Stack: ({ props, children }) =>\n    h(\n      \"div\",\n      {\n        class: [\n          \"json-render-stack\",\n          props.direction === \"horizontal\" && \"json-render-stack--horizontal\",\n          props.align && `json-render-stack--align-${props.align}`,\n        ]\n          .filter(Boolean)\n          .join(\" \"),\n        style: {\n          gap: props.gap ? `${props.gap}px` : undefined,\n          padding: props.padding ? `${props.padding}px` : undefined,\n        },\n      },\n      children,\n    ),\n\n  Card: ({ props, children }) =>\n    h(\"div\", { class: \"json-render-card\" }, [\n      props.title &&\n        h(\"div\", { class: \"json-render-card-title-wrap\" }, [\n          h(\"h2\", { class: \"json-render-card-title\" }, props.title),\n        ]),\n      props.subtitle &&\n        h(\"p\", { class: \"json-render-card-subtitle\" }, props.subtitle),\n      children,\n    ]),\n\n  Text: ({ props }) =>\n    h(\n      \"span\",\n      {\n        class: [\n          \"json-render-text\",\n          props.size &&\n            props.size !== \"md\" &&\n            `json-render-text--${props.size}`,\n          props.weight &&\n            props.weight !== \"normal\" &&\n            `json-render-text--${props.weight}`,\n        ]\n          .filter(Boolean)\n          .join(\" \"),\n        style: props.color ? { color: props.color } : undefined,\n      },\n      String(props.content ?? \"\"),\n    ),\n\n  Button: ({ props, emit }) =>\n    h(\n      \"button\",\n      {\n        disabled: props.disabled,\n        onClick: () => emit(\"press\"),\n        class: [\n          \"json-render-button\",\n          props.variant && `json-render-button--${props.variant}`,\n        ]\n          .filter(Boolean)\n          .join(\" \"),\n      },\n      props.label,\n    ),\n\n  Badge: ({ props }) =>\n    h(\n      \"span\",\n      {\n        class: \"json-render-badge\",\n        style: props.color\n          ? {\n              backgroundColor: `${props.color}20`,\n              color: props.color,\n              borderColor: `${props.color}40`,\n            }\n          : undefined,\n      },\n      props.label,\n    ),\n\n  ListItem: ({ props, emit }) =>\n    h(\n      \"div\",\n      {\n        onClick: () => emit(\"press\"),\n        class: [\n          \"json-render-list-item\",\n          props.completed && \"json-render-list-item--done\",\n        ]\n          .filter(Boolean)\n          .join(\" \"),\n      },\n      [\n        h(\n          \"div\",\n          {\n            class: [\n              \"json-render-list-item-check\",\n              props.completed && \"json-render-list-item-check--done\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          props.completed ? \"✓\" : \"\",\n        ),\n        h(\n          \"span\",\n          {\n            class: [\n              \"json-render-list-item-text\",\n              props.completed && \"json-render-list-item-text--done\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          props.title,\n        ),\n      ],\n    ),\n\n  RendererBadge: ({ props }) =>\n    h(\"span\", { class: \"json-render-renderer-badge\" }, [\n      h(\"span\", { class: \"json-render-renderer-dot\" }),\n      props.renderer === \"vue\"\n        ? \"Rendered with Vue\"\n        : props.renderer === \"react\"\n          ? \"Rendered with React\"\n          : props.renderer === \"svelte\"\n            ? \"Rendered with Svelte\"\n            : \"Rendered with Solid\",\n    ]),\n\n  RendererTabs: ({ props, emit }) =>\n    h(\"div\", { class: \"json-render-renderer-tabs-wrapper\" }, [\n      h(\"span\", { class: \"json-render-renderer-tabs-label\" }, \"Render\"),\n      h(\"div\", { class: \"json-render-renderer-tabs\" }, [\n        h(\n          \"button\",\n          {\n            onClick: () => emit(\"pressVue\"),\n            class: [\n              \"json-render-renderer-tab\",\n              props.renderer === \"vue\" && \"json-render-renderer-tab--active\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          \"Vue\",\n        ),\n        h(\n          \"button\",\n          {\n            onClick: () => emit(\"pressReact\"),\n            class: [\n              \"json-render-renderer-tab\",\n              props.renderer === \"react\" && \"json-render-renderer-tab--active\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          \"React\",\n        ),\n        h(\n          \"button\",\n          {\n            onClick: () => emit(\"pressSvelte\"),\n            class: [\n              \"json-render-renderer-tab\",\n              props.renderer === \"svelte\" && \"json-render-renderer-tab--active\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          \"Svelte\",\n        ),\n        h(\n          \"button\",\n          {\n            onClick: () => emit(\"pressSolid\"),\n            class: [\n              \"json-render-renderer-tab\",\n              props.renderer === \"solid\" && \"json-render-renderer-tab--active\",\n            ]\n              .filter(Boolean)\n              .join(\" \"),\n          },\n          \"Solid\",\n        ),\n      ]),\n    ]),\n};\n"
  },
  {
    "path": "examples/vite-renderers/svelte.config.js",
    "content": "export default {};\n"
  },
  {
    "path": "examples/vite-renderers/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.vue\", \"src/**/*.svelte\"]\n}\n"
  },
  {
    "path": "examples/vite-renderers/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport vue from \"@vitejs/plugin-vue\";\nimport react from \"@vitejs/plugin-react\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\nimport solid from \"vite-plugin-solid\";\n\nexport default defineConfig({\n  plugins: [\n    svelte(),\n    vue(),\n    react({ include: /src\\/react\\/.*\\.tsx$/ }),\n    solid({ include: [/src\\/solid\\/.*\\.tsx$/, /packages\\/solid\\/.*\\.tsx$/] }),\n  ],\n});\n"
  },
  {
    "path": "examples/vue/CHANGELOG.md",
    "content": "# example-vue\n\n## 0.1.7\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/vue@0.14.1\n\n## 0.1.6\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/vue@0.14.0\n\n## 0.1.5\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/vue@0.13.0\n\n## 0.1.4\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/vue@0.12.1\n\n## 0.1.3\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/vue@0.12.0\n\n## 0.1.2\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/vue@0.11.0\n\n## 0.1.1\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/vue@0.10.0\n"
  },
  {
    "path": "examples/vue/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>json-render vue example</title>\n    <meta property=\"og:title\" content=\"Vue Example | json-render\" />\n    <meta property=\"og:image\" content=\"/og-image.png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content=\"Vue Example | json-render\" />\n    <meta name=\"twitter:image\" content=\"/og-image.png\" />\n    <style>\n      * {\n        box-sizing: border-box;\n        margin: 0;\n        padding: 0;\n      }\n      body {\n        font-family:\n          -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n        background: #f9fafb;\n        color: #111827;\n        min-height: 100vh;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "examples/vue/package.json",
    "content": "{\n  \"name\": \"example-vue\",\n  \"version\": \"0.1.7\",\n  \"private\": true,\n  \"scripts\": {\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"portless vue.json-render vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/vue\": \"workspace:*\",\n    \"vue\": \"^3.5.0\",\n    \"zod\": \"4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@vitejs/plugin-vue\": \"^6.0.4\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^7.3.1\",\n    \"vue-tsc\": \"^3.2.5\"\n  }\n}\n"
  },
  {
    "path": "examples/vue/src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { StateProvider } from \"@json-render/vue\";\nimport { demoSpec } from \"./lib/spec\";\nimport DemoRenderer from \"./DemoRenderer.vue\";\n\nconst initialState = demoSpec.state ?? {};\n</script>\n\n<template>\n  <!-- StateProvider sets up the reactive state store for the whole tree -->\n  <StateProvider :initial-state=\"initialState\">\n    <DemoRenderer />\n  </StateProvider>\n</template>\n"
  },
  {
    "path": "examples/vue/src/DemoRenderer.vue",
    "content": "<script setup lang=\"ts\">\nimport {\n  ActionProvider,\n  ValidationProvider,\n  VisibilityProvider,\n  Renderer,\n  defineRegistry,\n  useStateStore,\n} from \"@json-render/vue\";\nimport { catalog } from \"./lib/catalog\";\nimport { components } from \"./lib/registry\";\nimport { demoSpec } from \"./lib/spec\";\n\n// Access the state store provided by the parent StateProvider\nconst { get, set } = useStateStore();\n\n// Build registry — include stub actions to satisfy catalog types.\n// The actual logic runs in the handlers below, which have direct\n// access to the state store.\nconst { registry } = defineRegistry(catalog, {\n  components,\n  actions: {\n    increment: async () => {},\n    decrement: async () => {},\n    reset: async () => {},\n    toggleItem: async () => {},\n  },\n});\n\n// Action handlers — close over the state store's get/set so they\n// can read and write state directly without needing an external store.\nconst handlers = {\n  increment: async () => {\n    set(\"/count\", Number(get(\"/count\") || 0) + 1);\n  },\n  decrement: async () => {\n    set(\"/count\", Math.max(0, Number(get(\"/count\") || 0) - 1));\n  },\n  reset: async () => {\n    set(\"/count\", 0);\n  },\n  toggleItem: async (params: Record<string, unknown>) => {\n    const index = params.index as number;\n    const todos = (\n      get(\"/todos\") as Array<{\n        id: number;\n        title: string;\n        completed: boolean;\n      }>\n    ).slice();\n    const item = todos[index];\n    console.log(\"item\", item);\n    if (item) {\n      todos[index] = { ...item, completed: !item.completed };\n    }\n    set(\"/todos\", todos);\n  },\n};\n</script>\n\n<template>\n  <ActionProvider :handlers=\"handlers\">\n    <VisibilityProvider>\n      <ValidationProvider>\n        <Renderer :spec=\"demoSpec\" :registry=\"registry\" />\n      </ValidationProvider>\n    </VisibilityProvider>\n  </ActionProvider>\n</template>\n"
  },
  {
    "path": "examples/vue/src/lib/catalog.ts",
    "content": "import { schema } from \"@json-render/vue/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = schema.createCatalog({\n  components: {\n    Stack: {\n      props: z.object({\n        gap: z.number().optional(),\n        padding: z.number().optional(),\n        direction: z.enum([\"vertical\", \"horizontal\"]).optional(),\n        align: z.enum([\"start\", \"center\", \"end\"]).optional(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Layout container that stacks children vertically or horizontally\",\n    },\n    Card: {\n      props: z.object({\n        title: z.string().optional(),\n        subtitle: z.string().optional(),\n      }),\n      slots: [\"default\"],\n      description: \"A card container with optional title and subtitle\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).optional(),\n        weight: z.enum([\"normal\", \"medium\", \"bold\"]).optional(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"Displays a text string\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).optional(),\n        disabled: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A clickable button that emits a 'press' event\",\n    },\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A small badge/tag label\",\n    },\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().optional(),\n        completed: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A single item in a list\",\n    },\n    Input: {\n      props: z.object({\n        value: z.string().optional(),\n        placeholder: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A text input field that supports two-way state binding\",\n    },\n  },\n  actions: {\n    increment: {\n      params: z.object({}),\n      description: \"Increment the counter by 1\",\n    },\n    decrement: {\n      params: z.object({}),\n      description: \"Decrement the counter by 1\",\n    },\n    reset: {\n      params: z.object({}),\n      description: \"Reset the counter to 0\",\n    },\n    toggleItem: {\n      params: z.object({\n        index: z.number(),\n      }),\n      description: \"Toggle the completed state of a todo item\",\n    },\n  },\n});\n\nexport type AppCatalog = typeof catalog;\n"
  },
  {
    "path": "examples/vue/src/lib/registry.ts",
    "content": "import { h } from \"vue\";\nimport type { Components } from \"@json-render/vue\";\nimport { useBoundProp } from \"@json-render/vue\";\nimport type { AppCatalog } from \"./catalog\";\n\nexport const components: Components<AppCatalog> = {\n  Stack: ({ props, children }) => {\n    const isHorizontal = props.direction === \"horizontal\";\n    return h(\n      \"div\",\n      {\n        style: {\n          display: \"flex\",\n          flexDirection: isHorizontal ? \"row\" : \"column\",\n          gap: props.gap ? `${props.gap}px` : undefined,\n          padding: props.padding ? `${props.padding}px` : undefined,\n          alignItems: props.align ?? (isHorizontal ? \"center\" : \"stretch\"),\n        },\n      },\n      children,\n    );\n  },\n\n  Card: ({ props, children }) =>\n    h(\n      \"div\",\n      {\n        style: {\n          backgroundColor: \"white\",\n          borderRadius: \"12px\",\n          border: \"1px solid #e5e7eb\",\n          padding: \"20px\",\n          boxShadow: \"0 1px 3px rgba(0,0,0,0.05)\",\n        },\n      },\n      [\n        props.title &&\n          h(\"div\", { style: { marginBottom: \"4px\" } }, [\n            h(\n              \"h2\",\n              {\n                style: {\n                  fontSize: \"16px\",\n                  fontWeight: \"600\",\n                  color: \"#111827\",\n                  margin: 0,\n                },\n              },\n              props.title,\n            ),\n          ]),\n        props.subtitle &&\n          h(\n            \"p\",\n            {\n              style: {\n                fontSize: \"13px\",\n                color: \"#6b7280\",\n                margin: \"0 0 12px 0\",\n              },\n            },\n            props.subtitle,\n          ),\n        children,\n      ],\n    ),\n\n  Text: ({ props }) => {\n    const sizeMap: Record<string, string> = {\n      sm: \"12px\",\n      md: \"14px\",\n      lg: \"16px\",\n      xl: \"24px\",\n    };\n    const weightMap: Record<string, string> = {\n      normal: \"400\",\n      medium: \"500\",\n      bold: \"700\",\n    };\n    return h(\n      \"span\",\n      {\n        style: {\n          fontSize: sizeMap[props.size ?? \"md\"] ?? \"14px\",\n          fontWeight: weightMap[props.weight ?? \"normal\"] ?? \"400\",\n          color: props.color ?? \"#111827\",\n        },\n      },\n      String(props.content ?? \"\"),\n    );\n  },\n\n  Button: ({ props, emit }) =>\n    h(\n      \"button\",\n      {\n        disabled: props.disabled,\n        onClick: () => emit(\"press\"),\n        style: {\n          padding: \"8px 16px\",\n          borderRadius: \"8px\",\n          border: \"none\",\n          cursor: props.disabled ? \"not-allowed\" : \"pointer\",\n          fontWeight: \"500\",\n          fontSize: \"14px\",\n          transition: \"background 0.15s\",\n          opacity: props.disabled ? \"0.5\" : \"1\",\n          backgroundColor:\n            props.variant === \"danger\"\n              ? \"#fee2e2\"\n              : props.variant === \"secondary\"\n                ? \"#f3f4f6\"\n                : \"#3b82f6\",\n          color:\n            props.variant === \"danger\"\n              ? \"#dc2626\"\n              : props.variant === \"secondary\"\n                ? \"#374151\"\n                : \"white\",\n        },\n      },\n      props.label,\n    ),\n\n  Badge: ({ props }) =>\n    h(\n      \"span\",\n      {\n        style: {\n          display: \"inline-block\",\n          padding: \"4px 12px\",\n          borderRadius: \"999px\",\n          fontSize: \"13px\",\n          fontWeight: \"500\",\n          backgroundColor: props.color ? `${props.color}20` : \"#e0f2fe\",\n          color: props.color ?? \"#0369a1\",\n          border: `1px solid ${props.color ? `${props.color}40` : \"#bae6fd\"}`,\n        },\n      },\n      props.label,\n    ),\n\n  Input: ({ props, bindings }) => {\n    const [value, setValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    return h(\"input\", {\n      value: value ?? \"\",\n      placeholder: props.placeholder as string | undefined,\n      onInput: (e: Event) => setValue((e.target as HTMLInputElement).value),\n      style: {\n        padding: \"8px 12px\",\n        borderRadius: \"8px\",\n        border: \"1px solid #d1d5db\",\n        fontSize: \"14px\",\n        outline: \"none\",\n        width: \"100%\",\n        boxSizing: \"border-box\",\n      },\n    });\n  },\n\n  ListItem: ({ props, emit }) =>\n    h(\n      \"div\",\n      {\n        onClick: () => emit(\"press\"),\n        style: {\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: \"12px\",\n          padding: \"10px 12px\",\n          borderRadius: \"8px\",\n          cursor: \"pointer\",\n          backgroundColor: props.completed ? \"#f0fdf4\" : \"#f9fafb\",\n          border: `1px solid ${props.completed ? \"#bbf7d0\" : \"#e5e7eb\"}`,\n        },\n      },\n      [\n        h(\n          \"div\",\n          {\n            style: {\n              width: \"18px\",\n              height: \"18px\",\n              borderRadius: \"50%\",\n              border: `2px solid ${props.completed ? \"#16a34a\" : \"#d1d5db\"}`,\n              backgroundColor: props.completed ? \"#16a34a\" : \"transparent\",\n              flexShrink: \"0\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontSize: \"11px\",\n              color: \"white\",\n            },\n          },\n          props.completed ? \"✓\" : \"\",\n        ),\n        h(\n          \"span\",\n          {\n            style: {\n              fontSize: \"14px\",\n              color: props.completed ? \"#6b7280\" : \"#111827\",\n              textDecoration: props.completed ? \"line-through\" : \"none\",\n            },\n          },\n          props.title,\n        ),\n      ],\n    ),\n};\n"
  },
  {
    "path": "examples/vue/src/lib/spec.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport const demoSpec: Spec = {\n  root: \"root\",\n  state: {\n    count: 0,\n    name: \"\",\n    todos: [\n      { id: 1, title: \"Learn Vue 3\", completed: true },\n      { id: 2, title: \"Try @json-render/vue\", completed: false },\n      { id: 3, title: \"Build something awesome\", completed: false },\n    ],\n  },\n  elements: {\n    root: {\n      type: \"Stack\",\n      props: { gap: 24, padding: 24, direction: \"vertical\" },\n      children: [\n        \"header\",\n        \"counter-card\",\n        \"milestone-badge\",\n        \"todos-card\",\n        \"input-card\",\n      ],\n    },\n    header: {\n      type: \"Text\",\n      props: {\n        content: \"@json-render/vue demo\",\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n\n    // ---- Counter card ----\n    \"counter-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Counter\",\n        subtitle: \"Click the buttons to change the count\",\n      },\n      children: [\"counter-body\"],\n    },\n    \"counter-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"horizontal\", align: \"center\" },\n      children: [\n        \"decrement-btn\",\n        \"counter-value\",\n        \"increment-btn\",\n        \"reset-btn\",\n      ],\n    },\n    \"decrement-btn\": {\n      type: \"Button\",\n      props: { label: \"−\", variant: \"secondary\" },\n      on: { press: { action: \"decrement\" } },\n    },\n    \"counter-value\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/count\" },\n        size: \"xl\",\n        weight: \"bold\",\n      },\n    },\n    \"increment-btn\": {\n      type: \"Button\",\n      props: { label: \"+\", variant: \"primary\" },\n      on: { press: { action: \"increment\" } },\n    },\n    \"reset-btn\": {\n      type: \"Button\",\n      props: { label: \"Reset\", variant: \"danger\" },\n      on: { press: { action: \"reset\" } },\n    },\n\n    // ---- Milestone badge (visible only when count >= 10) ----\n    \"milestone-badge\": {\n      type: \"Badge\",\n      props: { label: \"Milestone reached: 10!\", color: \"#10b981\" },\n      visible: { $state: \"/count\", gte: 10 },\n    },\n\n    // ---- Todos card ----\n    \"todos-card\": {\n      type: \"Card\",\n      props: { title: \"Todo List\", subtitle: \"Your tasks\" },\n      children: [\"todos-list\"],\n    },\n    \"todos-list\": {\n      type: \"Stack\",\n      props: { gap: 8, direction: \"vertical\" },\n      repeat: { statePath: \"/todos\", key: \"id\" },\n      children: [\"todo-item\"],\n    },\n    \"todo-item\": {\n      type: \"ListItem\",\n      props: {\n        title: { $item: \"title\" },\n        completed: { $item: \"completed\" },\n      },\n      on: {\n        press: { action: \"toggleItem\", params: { index: { $index: true } } },\n      },\n    },\n\n    // ---- Bound Input card (useBoundProp demo) ----\n    \"input-card\": {\n      type: \"Card\",\n      props: {\n        title: \"Bound Input\",\n        subtitle: \"Type to update state — the display text reacts in real time\",\n      },\n      children: [\"input-body\"],\n    },\n    \"input-body\": {\n      type: \"Stack\",\n      props: { gap: 12, direction: \"vertical\" },\n      children: [\"name-input\", \"name-display\"],\n    },\n    \"name-input\": {\n      type: \"Input\",\n      props: {\n        value: { $bindState: \"/name\" },\n        placeholder: \"Enter your name…\",\n      },\n    },\n    \"name-display\": {\n      type: \"Text\",\n      props: {\n        content: { $state: \"/name\" },\n        size: \"md\",\n        color: \"#6b7280\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "examples/vue/src/main.ts",
    "content": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\n\ncreateApp(App).mount(\"#app\");\n"
  },
  {
    "path": "examples/vue/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"preserve\",\n    \"strict\": true\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.vue\"]\n}\n"
  },
  {
    "path": "examples/vue/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport vue from \"@vitejs/plugin-vue\";\n\nexport default defineConfig({\n  plugins: [vue()],\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"json-render\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\"\n  },\n  \"homepage\": \"https://github.com/vercel-labs/json-render#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"predev\": \"command -v portless >/dev/null 2>&1 || (echo '\\\\nportless is required but not installed. Run: npm i -g portless\\\\nSee: https://github.com/vercel-labs/portless\\\\n' && exit 1)\",\n    \"dev\": \"turbo run dev --concurrency 20\",\n    \"lint\": \"turbo run lint\",\n    \"format\": \"prettier --write \\\"**/*.{ts,tsx}\\\"\",\n    \"type-check\": \"turbo run check-types\",\n    \"check-types\": \"turbo run check-types\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:coverage\": \"vitest run --coverage\",\n    \"test:e2e\": \"pnpm --filter e2e-tests test\",\n    \"prepare\": \"husky\",\n    \"changeset\": \"changeset\",\n    \"ci:version\": \"changeset version && pnpm install --no-frozen-lockfile\",\n    \"ci:publish\": \"pnpm run build && changeset publish\",\n    \"generate:og\": \"npx tsx scripts/generate-og-images.mts\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"2.29.8\",\n    \"@resvg/resvg-js\": \"2.6.2\",\n    \"@solidjs/testing-library\": \"^0.8.10\",\n    \"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n    \"@testing-library/dom\": \"^10.4.1\",\n    \"@testing-library/react\": \"^16.3.1\",\n    \"@testing-library/svelte\": \"^5.2.0\",\n    \"@types/react\": \"^19.2.3\",\n    \"husky\": \"^9.1.7\",\n    \"jsdom\": \"^27.4.0\",\n    \"lint-staged\": \"^16.2.7\",\n    \"prettier\": \"^3.7.4\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"satori\": \"0.25.0\",\n    \"solid-js\": \"^1.9.11\",\n    \"svelte\": \"^5.0.0\",\n    \"turbo\": \"^2.7.4\",\n    \"typescript\": \"5.9.2\",\n    \"vite-plugin-solid\": \"^2.11.10\",\n    \"vitest\": \"^4.0.17\"\n  },\n  \"packageManager\": \"pnpm@10.29.3\",\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": \"prettier --write\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"examples/*\",\n    \"examples/stripe-app/*\",\n    \"packages/*\",\n    \"tests/*\"\n  ]\n}\n"
  },
  {
    "path": "packages/codegen/CHANGELOG.md",
    "content": "# @json-render/codegen\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n\n## 0.8.0\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n\n## 0.7.0\n\n### Patch Changes\n\n- Updated dependencies [2d70fab]\n  - @json-render/core@0.7.0\n\n## 0.6.1\n\n### Patch Changes\n\n- @json-render/core@0.6.1\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [ea97aff]\n  - @json-render/core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 06b8745: Chat mode (inline GenUI), AI SDK integration, two-way binding, and expression-based visibility/props.\n\n  ### New: Chat Mode (Inline GenUI)\n\n  Two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets AI respond conversationally with embedded UI specs — ideal for chatbots and copilot experiences.\n  - `catalog.prompt({ mode: \"chat\" })` generates a chat-aware system prompt\n  - `pipeJsonRender()` server-side transform separates text from JSONL patches in a mixed stream\n  - `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n\n  ### New: AI SDK Integration\n\n  First-class Vercel AI SDK support with typed data parts and stream utilities.\n  - `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n  - `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n  - `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n  ### New: React Chat Hooks\n  - `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n  - `useJsonRenderMessage()` — extract spec + text from a message's parts array\n  - `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n  - `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem` expressions\n\n  ### New: Two-Way Binding\n\n  Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state.\n\n  ### New: Expression-Based Props and Visibility\n\n  Replaced string token rewriting with structured expression objects:\n  - Props: `{ $state: \"/path\" }`, `{ $item: \"field\" }`, `{ $index: true }`\n  - Visibility: `{ $state: \"/path\", eq: \"value\" }`, `{ $item: \"active\" }`, `{ $index: true, gt: 0 }`\n  - Logic: `{ $and: [...] }`, `{ $or: [...] }`, and implicit AND via arrays\n  - Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`\n\n  ### New: Utilities\n  - `applySpecPatch()` — typed convenience wrapper for applying a single patch to a Spec\n  - `nestedToFlat()` — convert nested tree specs to flat `{ root, elements }` format\n  - `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n  ### New: Chat Example\n\n  Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming UI generation.\n\n  ### Improved: Renderer\n  - `ElementRenderer` is now `React.memo`'d for better performance in repeat lists\n  - `emit` is always defined (never `undefined`) — no more optional chaining needed\n  - Action params are resolved through `resolveActionParam` supporting `$item`, `$index`, `$state`\n  - Repeat scope now passes the actual item object instead of requiring token rewriting\n\n  ### Breaking Changes\n  - **Expressions renamed**: `{ $path }` / `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }`\n  - **Visibility conditions**: `{ path }` → `{ $state }`, `{ and/or/not }` → `{ $and/$or }` with `not` as operator flag\n  - **DynamicValue**: `{ path: string }` → `{ $state: string }`\n  - **Repeat field**: `repeat.path` → `repeat.statePath`\n  - **Action params**: `path` → `statePath` in setState action params\n  - **Provider props**: `actionHandlers` → `handlers` on `JSONUIProvider`/`ActionProvider`\n  - **Auth removed**: `AuthState` type and `{ auth }` visibility conditions removed — model auth as regular state\n  - **Legacy catalog removed**: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`, `ComponentDefinition`, `CatalogConfig`, `SystemPromptOptions` removed\n  - **React exports removed**: `createRendererFromCatalog`, `rewriteRepeatTokens`\n  - **Codegen**: `traverseTree` → `traverseSpec`, `SpecVisitor` → `TreeVisitor`\n\n### Patch Changes\n\n- Updated dependencies [06b8745]\n  - @json-render/core@0.6.0\n\n## 0.5.2\n\n### Patch Changes\n\n- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.\n- Updated dependencies [429e456]\n  - @json-render/core@0.5.2\n\n## 0.5.1\n\n### Patch Changes\n\n- @json-render/core@0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.\n\n### Patch Changes\n\n- Updated dependencies [3d2d1ad]\n  - @json-render/core@0.5.0\n\n## 0.4.4\n\n### Patch Changes\n\n- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream\n- Updated dependencies [dd17549]\n  - @json-render/core@0.4.4\n\n## 0.4.3\n\n### Patch Changes\n\n- 61ee8e5: include remove op in system prompt\n- Updated dependencies [61ee8e5]\n  - @json-render/core@0.4.3\n\n## 0.4.2\n\n### Patch Changes\n\n- 54bce09: add defineRegistry function\n- Updated dependencies [54bce09]\n  - @json-render/core@0.4.2\n"
  },
  {
    "path": "packages/codegen/README.md",
    "content": "# @json-render/codegen\n\nUtilities for generating code from json-render UI trees.\n\nThis package provides framework-agnostic utilities for building code generators. Use these utilities to create custom code exporters for your specific framework (Next.js, Remix, etc.).\n\n## Installation\n\n```bash\nnpm install @json-render/codegen\n# or\npnpm add @json-render/codegen\n```\n\n## Utilities\n\n### Tree Traversal\n\n```typescript\nimport { traverseSpec, collectUsedComponents, collectStatePaths, collectActions } 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 { serializePropValue, serializeProps, escapeString } from '@json-render/codegen';\n\n// Serialize a single value\nserializePropValue(\"hello\");\n// { value: '\"hello\"', needsBraces: false }\n\nserializePropValue(42);\n// { value: '42', needsBraces: true }\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\n### Types\n\n```typescript\nimport type { GeneratedFile, CodeGenerator } from '@json-render/codegen';\n\n// Implement your own code generator\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\nSee the `examples/dashboard` for a complete example of building a Next.js code generator using these utilities.\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  \n  // Generate package.json\n  files.push({\n    path: 'package.json',\n    content: JSON.stringify({\n      name: 'my-generated-app',\n      dependencies: {\n        next: '^14.0.0',\n        react: '^18.0.0',\n      }\n    }, null, 2)\n  });\n  \n  // Generate component files...\n  // Generate main page...\n  \n  return files;\n}\n```\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/codegen/package.json",
    "content": "{\n  \"name\": \"@json-render/codegen\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Utilities for generating code from json-render UI trees\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"codegen\",\n    \"code-generation\",\n    \"export\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/codegen\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "packages/codegen/src/index.ts",
    "content": "export {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n  type TreeVisitor,\n} from \"./traverse\";\n\nexport {\n  serializePropValue,\n  serializeProps,\n  escapeString,\n  type SerializeOptions,\n} from \"./serialize\";\n\nexport type { GeneratedFile, CodeGenerator } from \"./types\";\n"
  },
  {
    "path": "packages/codegen/src/serialize.ts",
    "content": "/**\n * Options for serialization\n */\nexport interface SerializeOptions {\n  /** Quote style for strings */\n  quotes?: \"single\" | \"double\";\n  /** Indent for objects/arrays */\n  indent?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<SerializeOptions> = {\n  quotes: \"double\",\n  indent: 2,\n};\n\n/**\n * Escape a string for use in code\n */\nexport function escapeString(\n  str: string,\n  quotes: \"single\" | \"double\" = \"double\",\n): string {\n  const quoteChar = quotes === \"single\" ? \"'\" : '\"';\n  const escaped = str\n    .replace(/\\\\/g, \"\\\\\\\\\")\n    .replace(/\\n/g, \"\\\\n\")\n    .replace(/\\r/g, \"\\\\r\")\n    .replace(/\\t/g, \"\\\\t\");\n\n  if (quotes === \"single\") {\n    return escaped.replace(/'/g, \"\\\\'\");\n  }\n  return escaped.replace(/\"/g, '\\\\\"');\n}\n\n/**\n * Serialize a single prop value to a code string\n *\n * @returns Object with `value` (the serialized string) and `needsBraces` (whether JSX needs {})\n */\nexport function serializePropValue(\n  value: unknown,\n  options: SerializeOptions = {},\n): { value: string; needsBraces: boolean } {\n  const opts = { ...DEFAULT_OPTIONS, ...options };\n  const q = opts.quotes === \"single\" ? \"'\" : '\"';\n\n  if (value === null) {\n    return { value: \"null\", needsBraces: true };\n  }\n\n  if (value === undefined) {\n    return { value: \"undefined\", needsBraces: true };\n  }\n\n  if (typeof value === \"string\") {\n    return {\n      value: `${q}${escapeString(value, opts.quotes)}${q}`,\n      needsBraces: false,\n    };\n  }\n\n  if (typeof value === \"number\") {\n    return { value: String(value), needsBraces: true };\n  }\n\n  if (typeof value === \"boolean\") {\n    if (value === true) {\n      return { value: \"true\", needsBraces: false }; // Can use shorthand\n    }\n    return { value: \"false\", needsBraces: true };\n  }\n\n  if (Array.isArray(value)) {\n    const items = value.map((v) => serializePropValue(v, opts).value);\n    return { value: `[${items.join(\", \")}]`, needsBraces: true };\n  }\n\n  if (typeof value === \"object\") {\n    // Check for $state reference\n    if (\n      \"$state\" in value &&\n      typeof (value as { $state: unknown }).$state === \"string\"\n    ) {\n      return {\n        value: `{ $state: ${q}${escapeString((value as { $state: string }).$state, opts.quotes)}${q} }`,\n        needsBraces: true,\n      };\n    }\n\n    const entries = Object.entries(value)\n      .filter(([, v]) => v !== undefined)\n      .map(([k, v]) => {\n        const serialized = serializePropValue(v, opts).value;\n        // Use shorthand if key matches value for simple identifiers\n        return `${k}: ${serialized}`;\n      });\n\n    return { value: `{ ${entries.join(\", \")} }`, needsBraces: true };\n  }\n\n  return { value: String(value), needsBraces: true };\n}\n\n/**\n * Serialize props object to JSX attributes string\n */\nexport function serializeProps(\n  props: Record<string, unknown>,\n  options: SerializeOptions = {},\n): string {\n  const parts: string[] = [];\n\n  for (const [key, value] of Object.entries(props)) {\n    if (value === undefined || value === null) continue;\n\n    const serialized = serializePropValue(value, options);\n\n    // Boolean true can be shorthand\n    if (typeof value === \"boolean\" && value === true) {\n      parts.push(key);\n    } else if (serialized.needsBraces) {\n      parts.push(`${key}={${serialized.value}}`);\n    } else {\n      parts.push(`${key}=${serialized.value}`);\n    }\n  }\n\n  return parts.join(\" \");\n}\n"
  },
  {
    "path": "packages/codegen/src/traverse.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport {\n  traverseSpec,\n  collectUsedComponents,\n  collectStatePaths,\n  collectActions,\n} from \"./traverse\";\nimport type { Spec } from \"@json-render/core\";\n\ndescribe(\"traverseSpec\", () => {\n  it(\"visits all elements depth-first\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Card\",\n          props: {},\n          children: [\"child1\", \"child2\"],\n        },\n        child1: {\n          type: \"Text\",\n          props: {},\n        },\n        child2: {\n          type: \"Button\",\n          props: {},\n        },\n      },\n    };\n\n    const visited: string[] = [];\n    traverseSpec(spec, (_element, key) => {\n      visited.push(key);\n    });\n\n    expect(visited).toEqual([\"root\", \"child1\", \"child2\"]);\n  });\n\n  it(\"handles empty spec\", () => {\n    const visited: string[] = [];\n    traverseSpec(null as unknown as Spec, (_element, key) => {\n      visited.push(key);\n    });\n    expect(visited).toEqual([]);\n  });\n});\n\ndescribe(\"collectUsedComponents\", () => {\n  it(\"collects unique component types\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Card\",\n          props: {},\n          children: [\"child1\", \"child2\"],\n        },\n        child1: {\n          type: \"Text\",\n          props: {},\n        },\n        child2: {\n          type: \"Text\",\n          props: {},\n        },\n      },\n    };\n\n    const components = collectUsedComponents(spec);\n    expect(components).toEqual(new Set([\"Card\", \"Text\"]));\n  });\n});\n\ndescribe(\"collectStatePaths\", () => {\n  it(\"collects paths from statePath props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Metric\",\n          props: { statePath: \"analytics/revenue\" },\n        },\n      },\n    };\n\n    const paths = collectStatePaths(spec);\n    expect(paths).toEqual(new Set([\"analytics/revenue\"]));\n  });\n\n  it(\"collects paths from dynamic value objects\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Text\",\n          props: { content: { $state: \"/user/name\" } },\n        },\n      },\n    };\n\n    const paths = collectStatePaths(spec);\n    expect(paths).toEqual(new Set([\"/user/name\"]));\n  });\n});\n\ndescribe(\"collectActions\", () => {\n  it(\"collects action names from props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: { action: \"submit_form\" },\n        },\n      },\n    };\n\n    const actions = collectActions(spec);\n    expect(actions).toEqual(new Set([\"submit_form\"]));\n  });\n\n  it(\"collects actions from on event bindings\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: {},\n          on: { press: { action: \"submitForm\" } },\n        },\n      },\n    };\n\n    const actions = collectActions(spec);\n    expect(actions).toEqual(new Set([\"submitForm\"]));\n  });\n\n  it(\"collects actions from on array bindings\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: {},\n          on: {\n            press: [{ action: \"save\" }, { action: \"navigate\" }],\n          },\n        },\n      },\n    };\n\n    const actions = collectActions(spec);\n    expect(actions).toEqual(new Set([\"save\", \"navigate\"]));\n  });\n\n  it(\"collects actions from both props and on\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: { action: \"submit_form\" },\n          on: { press: { action: \"setState\", params: { statePath: \"/x\" } } },\n        },\n      },\n    };\n\n    const actions = collectActions(spec);\n    expect(actions).toEqual(new Set([\"submit_form\", \"setState\"]));\n  });\n});\n"
  },
  {
    "path": "packages/codegen/src/traverse.ts",
    "content": "import type { Spec, UIElement } from \"@json-render/core\";\n\n/**\n * Visitor function for spec traversal\n */\nexport interface TreeVisitor {\n  (\n    element: UIElement,\n    key: string,\n    depth: number,\n    parent: UIElement | null,\n  ): void;\n}\n\n/**\n * Traverse a UI spec depth-first\n */\nexport function traverseSpec(\n  spec: Spec,\n  visitor: TreeVisitor,\n  startKey?: string,\n): void {\n  if (!spec || !spec.root) return;\n\n  const rootKey = startKey ?? spec.root;\n  const rootElement = spec.elements[rootKey];\n  if (!rootElement) return;\n\n  function visit(key: string, depth: number, parent: UIElement | null): void {\n    const element = spec.elements[key];\n    if (!element) return;\n\n    visitor(element, key, depth, parent);\n\n    if (element.children) {\n      for (const childKey of element.children) {\n        visit(childKey, depth + 1, element);\n      }\n    }\n  }\n\n  visit(rootKey, 0, null);\n}\n\n/**\n * Collect all unique component types used in a spec\n */\nexport function collectUsedComponents(spec: Spec): Set<string> {\n  const components = new Set<string>();\n\n  traverseSpec(spec, (element, _key) => {\n    components.add(element.type);\n  });\n\n  return components;\n}\n\n/**\n * Collect all state paths referenced in a spec\n */\nexport function collectStatePaths(spec: Spec): Set<string> {\n  const paths = new Set<string>();\n\n  traverseSpec(spec, (element, _key) => {\n    // Check props for data paths\n    for (const [propName, propValue] of Object.entries(element.props)) {\n      // Check for path props (e.g., statePath, dataPath, bindPath)\n      if (typeof propValue === \"string\") {\n        if (\n          propName.endsWith(\"Path\") ||\n          propName === \"bindPath\" ||\n          propName === \"statePath\"\n        ) {\n          paths.add(propValue);\n        }\n      }\n\n      // Check for dynamic value objects with $state\n      if (\n        propValue &&\n        typeof propValue === \"object\" &&\n        \"$state\" in propValue &&\n        typeof (propValue as { $state: unknown }).$state === \"string\"\n      ) {\n        paths.add((propValue as { $state: string }).$state);\n      }\n    }\n\n    // Check visibility conditions for $state paths\n    if (element.visible != null && typeof element.visible !== \"boolean\") {\n      collectPathsFromCondition(element.visible, paths);\n    }\n  });\n\n  return paths;\n}\n\nfunction collectPathFromItem(\n  item: Record<string, unknown>,\n  paths: Set<string>,\n): void {\n  if (typeof item.$state === \"string\") {\n    paths.add(item.$state);\n  }\n  // Also collect $state references in comparison values (eq, neq, etc.)\n  for (const op of [\"eq\", \"neq\", \"gt\", \"gte\", \"lt\", \"lte\"]) {\n    const val = item[op];\n    if (\n      val &&\n      typeof val === \"object\" &&\n      \"$state\" in (val as Record<string, unknown>) &&\n      typeof (val as Record<string, unknown>).$state === \"string\"\n    ) {\n      paths.add((val as { $state: string }).$state);\n    }\n  }\n}\n\nfunction collectPathsFromCondition(\n  condition: unknown,\n  paths: Set<string>,\n): void {\n  if (!condition || typeof condition !== \"object\") return;\n\n  // Array = implicit AND\n  if (Array.isArray(condition)) {\n    for (const item of condition) {\n      if (item && typeof item === \"object\") {\n        collectPathFromItem(item as Record<string, unknown>, paths);\n      }\n    }\n    return;\n  }\n\n  const cond = condition as Record<string, unknown>;\n\n  // $or: recurse into each child condition\n  if (\"$or\" in cond && Array.isArray(cond.$or)) {\n    for (const child of cond.$or) {\n      collectPathsFromCondition(child, paths);\n    }\n    return;\n  }\n\n  // Single StateCondition\n  collectPathFromItem(cond, paths);\n}\n\n/**\n * Collect all action names used in a spec\n */\nexport function collectActions(spec: Spec): Set<string> {\n  const actions = new Set<string>();\n\n  traverseSpec(spec, (element, _key) => {\n    for (const propValue of Object.values(element.props)) {\n      // Check for action prop (string action name)\n      if (typeof propValue === \"string\" && propValue.startsWith(\"action:\")) {\n        actions.add(propValue.slice(7));\n      }\n\n      // Check for action objects\n      if (\n        propValue &&\n        typeof propValue === \"object\" &&\n        \"name\" in propValue &&\n        typeof (propValue as { name: unknown }).name === \"string\"\n      ) {\n        actions.add((propValue as { name: string }).name);\n      }\n    }\n\n    // Also check direct action prop\n    const actionProp = element.props.action;\n    if (typeof actionProp === \"string\") {\n      actions.add(actionProp);\n    }\n\n    // Collect actions from on event bindings\n    const onBindings = element.on;\n    if (onBindings) {\n      for (const binding of Object.values(onBindings)) {\n        const bindings = Array.isArray(binding) ? binding : [binding];\n        for (const b of bindings) {\n          if (\n            b &&\n            typeof b === \"object\" &&\n            \"action\" in b &&\n            typeof (b as { action: unknown }).action === \"string\"\n          ) {\n            actions.add((b as { action: string }).action);\n          }\n        }\n      }\n    }\n  });\n\n  return actions;\n}\n"
  },
  {
    "path": "packages/codegen/src/types.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\n/**\n * Represents a generated file\n */\nexport interface GeneratedFile {\n  /** File path relative to project root */\n  path: string;\n  /** File contents */\n  content: string;\n}\n\n/**\n * Interface for code generators\n */\nexport interface CodeGenerator {\n  /** Generate files from a UI spec */\n  generate(spec: Spec): GeneratedFile[];\n}\n"
  },
  {
    "path": "packages/codegen/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/codegen/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"@json-render/core\"],\n});\n"
  },
  {
    "path": "packages/core/CHANGELOG.md",
    "content": "# @json-render/core\n\n## 0.14.1\n\n### Patch Changes\n\n- 43b7515: Add yaml format support to `buildUserPrompt`\n\n  ### New:\n  - `buildUserPrompt` now accepts `format` and `serializer` options, enabling YAML as a wire format alongside JSON\n\n## 0.14.0\n\n### Minor Changes\n\n- a8afd8b: Add YAML wire format package and universal edit modes for surgical spec refinement.\n\n  ### New:\n  - **`@json-render/yaml`** -- YAML wire format for json-render. Includes streaming YAML parser, `yamlPrompt()` for system prompts, and AI SDK transform (`pipeYamlRender`) as a drop-in alternative to JSONL streaming. Supports four fence types: `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff`.\n  - **Universal edit modes** in `@json-render/core` -- three strategies for multi-turn spec refinement: `patch` (RFC 6902), `merge` (RFC 7396), and `diff` (unified diff). New `editModes` option on `buildUserPrompt()` and `PromptOptions`. New helpers: `deepMergeSpec()`, `diffToPatches()`, `buildEditUserPrompt()`, `buildEditInstructions()`, `isNonEmptySpec()`.\n\n  ### Improved:\n  - **Playground** -- format toggle (JSONL / YAML), edit mode picker (patch / merge / diff), and token usage display with prompt caching stats.\n  - **Prompt caching** -- generate API uses Anthropic ephemeral cache control for system prompts.\n  - **CI** -- lint, type-check, and test jobs now run in parallel.\n\n## 0.13.0\n\n### Minor Changes\n\n- 5b32de8: Add SolidJS and React Three Fiber renderers, plus strict JSON Schema mode for LLM structured outputs.\n\n  ### New:\n  - **`@json-render/solid`** -- SolidJS renderer. JSON becomes Solid components with reactive rendering, schema export, and full catalog support.\n  - **`@json-render/react-three-fiber`** -- React Three Fiber renderer. JSON becomes 3D scenes with 19 built-in components for meshes, lights, models, environments, text, cameras, and controls.\n\n  ### Improved:\n  - **`@json-render/core`** -- `jsonSchema({ strict: true })` produces a JSON Schema subset compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic). Ensures `additionalProperties: false` on every object and all properties listed in `required`.\n\n## 0.12.1\n\n### Patch Changes\n\n- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.\n\n  ### Changed:\n  - **`@json-render/core`** — Renamed generation modes from `\"generate\"` / `\"chat\"` to `\"standalone\"` / `\"inline\"`. The old names still work but emit a deprecation warning.\n\n  ### Fixed:\n  - **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.\n\n  ### Other:\n  - Updated `homepage` URLs across all packages to point to `https://json-render.dev`.\n  - Reorganized skills directory structure for cleaner naming.\n  - Added skills documentation page to the web app.\n\n## 0.12.0\n\n### Minor Changes\n\n- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.\n\n  ### New:\n  - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.\n  - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.\n  - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.\n\n  ### Fixed:\n  - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.\n\n## 0.11.0\n\n### Minor Changes\n\n- 3f1e71e: Image renderer: generate SVG and PNG from JSON specs.\n\n  ### New: `@json-render/image` Package\n\n  Server-side image renderer powered by Satori. Turns the same `{ root, elements }` spec format into SVG or PNG output for OG images, social cards, and banners.\n  - `renderToSvg(spec, options)` — render spec to SVG string\n  - `renderToPng(spec, options)` — render spec to PNG buffer (requires `@resvg/resvg-js`)\n  - 9 standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer\n  - `standardComponentDefinitions` catalog for AI prompt generation\n  - Server-safe import path: `@json-render/image/server`\n  - Sub-path exports: `/render`, `/catalog`, `/server`\n\n## 0.10.0\n\n### Minor Changes\n\n- 9cef4e9: Dynamic forms, Vue renderer, XState Store adapter, and computed values.\n\n  ### New: `@json-render/vue` Package\n\n  Vue 3 renderer for json-render. Full feature parity with `@json-render/react` including data binding, visibility conditions, actions, validation, repeat scopes, and streaming.\n  - `defineRegistry` — create type-safe component registries from catalogs\n  - `Renderer` — render specs as Vue component trees\n  - Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`\n  - Composables: `useStateStore`, `useStateValue`, `useStateBinding`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`\n  - Streaming: `useUIStream`, `useChatUI`\n  - External store support via `StateStore` interface\n\n  ### New: `@json-render/xstate` Package\n\n  XState Store (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend.\n  - `xstateStoreStateStore({ atom })` — creates a `StateStore` from an `@xstate/store` atom\n  - Requires `@xstate/store` v3+\n\n  ### New: `$computed` Expressions\n\n  Call registered functions from prop expressions:\n  - `{ \"$computed\": \"functionName\", \"args\": { \"key\": <expression> } }` — calls a named function with resolved args\n  - Functions registered via catalog and provided at runtime through `functions` prop on `JSONUIProvider` / `createRenderer`\n  - `ComputedFunction` type exported from `@json-render/core`\n\n  ### New: `$template` Expressions\n\n  Interpolate state values into strings:\n  - `{ \"$template\": \"Hello, ${/user/name}!\" }` — replaces `${/path}` references with state values\n  - Missing paths resolve to empty string\n\n  ### New: State Watchers\n\n  React to state changes by triggering actions:\n  - `watch` field on elements maps state paths to action bindings\n  - Fires when watched values change (not on initial render)\n  - Supports cascading dependencies (e.g. country → city loading)\n  - `watch` is a top-level field on elements (sibling of type/props/children), not inside props\n  - Spec validator detects and auto-fixes `watch` placed inside props\n\n  ### New: Cross-Field Validation Functions\n\n  New built-in validation functions for cross-field comparisons:\n  - `equalTo` — alias for `matches` with clearer semantics\n  - `lessThan` — value must be less than another field (numbers, strings, coerced)\n  - `greaterThan` — value must be greater than another field\n  - `requiredIf` — required only when a condition field is truthy\n  - Validation args now resolve through `resolvePropValue` for consistent `$state` expression handling\n\n  ### New: `validateForm` Action (React)\n\n  Built-in action that validates all registered form fields at once:\n  - Runs `validateAll()` synchronously and writes `{ valid, errors }` to state\n  - Default state path: `/formValidation` (configurable via `statePath` param)\n  - Added to React schema's built-in actions list\n\n  ### Improved: shadcn/ui Validation\n\n  All form components now support validation:\n  - Checkbox, Radio, Switch — added `checks` and `validateOn` props\n  - Input, Textarea, Select — added `validateOn` prop (controls timing: change/blur/submit)\n  - Shared validation schemas reduce catalog definition duplication\n\n  ### Improved: React Provider Tree\n\n  Reordered provider nesting so `ValidationProvider` wraps `ActionProvider`, enabling `validateForm` to access validation state. Added `useOptionalValidation` hook for non-throwing access.\n\n## 0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n## 0.8.0\n\n### Minor Changes\n\n- 09376db: New `@json-render/react-pdf` package for generating PDF documents from JSON specs.\n\n  ### New: `@json-render/react-pdf`\n\n  PDF renderer for json-render, powered by `@react-pdf/renderer`. Define catalogs and registries the same way as `@json-render/react`, but output PDF documents instead of web UI.\n  - `renderToBuffer(spec)` — render a spec to an in-memory PDF buffer\n  - `renderToStream(spec)` — render to a readable stream (pipe to HTTP response)\n  - `renderToFile(spec, path)` — render directly to a file on disk\n  - `defineRegistry` / `createRenderer` — same API as `@json-render/react` for custom components\n  - `standardComponentDefinitions` — Zod-based catalog definitions (server-safe via `@json-render/react-pdf/catalog`)\n  - `standardComponents` — React PDF implementations for all standard components\n  - Server-safe import via `@json-render/react-pdf/server`\n\n  Standard components:\n  - **Document structure**: Document, Page\n  - **Layout**: View, Row, Column\n  - **Content**: Heading, Text, Image, Link\n  - **Data**: Table, List\n  - **Decorative**: Divider, Spacer\n  - **Page-level**: PageNumber\n\n  Includes full context support: state management, visibility conditions, actions, validation, and repeat scopes — matching the capabilities of `@json-render/react`.\n\n## 0.7.0\n\n### Minor Changes\n\n- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.\n\n  ### New: `@json-render/shadcn` Package\n\n  Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.\n  - `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)\n  - `shadcnComponents` — React implementations for all components\n  - Layout: Card, Stack, Grid, Separator\n  - Navigation: Tabs, Accordion, Collapsible, Pagination\n  - Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu\n  - Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table\n  - Feedback: Progress, Skeleton, Spinner\n  - Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup\n\n  ### New: Event Handles (`on()`)\n\n  Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:\n  - `emit()` — fire the event\n  - `shouldPreventDefault` — whether any action binding requested `preventDefault`\n  - `bound` — whether any handler is bound to this event\n\n  ### New: `BaseComponentProps`\n\n  Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.\n\n  ### New: Built-in Actions in Schema\n\n  Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.\n\n  ### New: `preventDefault` on `ActionBinding`\n\n  Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.\n\n  ### Improved: Stream Transform Text Block Splitting\n\n  `createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.\n\n  ### Improved: `defineRegistry` Actions Requirement\n\n  `defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.\n\n## 0.6.1\n\n## 0.6.1\n\n### Patch Changes\n\n- ea97aff: Fix infinite re-render loop caused by multiple unbound form inputs (Input, Textarea, Select) all registering field validation at the same empty path with different `checks` configs, causing them to overwrite each other endlessly. Stabilize context values in ActionProvider, ValidationProvider, and useUIStream by using refs for state/callbacks, preventing unnecessary re-render cascades on every state update.\n\n## 0.6.0\n\n### Minor Changes\n\n- 06b8745: Chat mode (inline GenUI), AI SDK integration, two-way binding, and expression-based visibility/props.\n\n  ### New: Chat Mode (Inline GenUI)\n\n  Two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets AI respond conversationally with embedded UI specs — ideal for chatbots and copilot experiences.\n  - `catalog.prompt({ mode: \"chat\" })` generates a chat-aware system prompt\n  - `pipeJsonRender()` server-side transform separates text from JSONL patches in a mixed stream\n  - `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n\n  ### New: AI SDK Integration\n\n  First-class Vercel AI SDK support with typed data parts and stream utilities.\n  - `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n  - `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n  - `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n  ### New: React Chat Hooks\n  - `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n  - `useJsonRenderMessage()` — extract spec + text from a message's parts array\n  - `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n  - `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem` expressions\n\n  ### New: Two-Way Binding\n\n  Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state.\n\n  ### New: Expression-Based Props and Visibility\n\n  Replaced string token rewriting with structured expression objects:\n  - Props: `{ $state: \"/path\" }`, `{ $item: \"field\" }`, `{ $index: true }`\n  - Visibility: `{ $state: \"/path\", eq: \"value\" }`, `{ $item: \"active\" }`, `{ $index: true, gt: 0 }`\n  - Logic: `{ $and: [...] }`, `{ $or: [...] }`, and implicit AND via arrays\n  - Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`\n\n  ### New: Utilities\n  - `applySpecPatch()` — typed convenience wrapper for applying a single patch to a Spec\n  - `nestedToFlat()` — convert nested tree specs to flat `{ root, elements }` format\n  - `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n  ### New: Chat Example\n\n  Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming UI generation.\n\n  ### Improved: Renderer\n  - `ElementRenderer` is now `React.memo`'d for better performance in repeat lists\n  - `emit` is always defined (never `undefined`) — no more optional chaining needed\n  - Action params are resolved through `resolveActionParam` supporting `$item`, `$index`, `$state`\n  - Repeat scope now passes the actual item object instead of requiring token rewriting\n\n  ### Breaking Changes\n  - **Expressions renamed**: `{ $path }` / `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }`\n  - **Visibility conditions**: `{ path }` → `{ $state }`, `{ and/or/not }` → `{ $and/$or }` with `not` as operator flag\n  - **DynamicValue**: `{ path: string }` → `{ $state: string }`\n  - **Repeat field**: `repeat.path` → `repeat.statePath`\n  - **Action params**: `path` → `statePath` in setState action params\n  - **Provider props**: `actionHandlers` → `handlers` on `JSONUIProvider`/`ActionProvider`\n  - **Auth removed**: `AuthState` type and `{ auth }` visibility conditions removed — model auth as regular state\n  - **Legacy catalog removed**: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`, `ComponentDefinition`, `CatalogConfig`, `SystemPromptOptions` removed\n  - **React exports removed**: `createRendererFromCatalog`, `rewriteRepeatTokens`\n  - **Codegen**: `traverseTree` → `traverseSpec`, `SpecVisitor` → `TreeVisitor`\n\n## 0.5.2\n\n### Patch Changes\n\n- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.\n\n## 0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.\n\n## 0.4.4\n\n### Patch Changes\n\n- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream\n\n## 0.4.3\n\n### Patch Changes\n\n- 61ee8e5: include remove op in system prompt\n\n## 0.4.2\n\n### Patch Changes\n\n- 54bce09: add defineRegistry function\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# @json-render/core\n\nCore library for json-render. Define schemas, create catalogs, generate AI prompts, and stream specs.\n\n## Installation\n\n```bash\nnpm install @json-render/core zod\n```\n\n## Key Concepts\n\n- **Schema**: Defines the structure of specs and catalogs\n- **Catalog**: Maps component/action names to their definitions with Zod props\n- **Spec**: JSON output from AI that conforms to the schema\n- **SpecStream**: JSONL streaming format for progressive spec building\n\n## Quick Start\n\n### Define a Schema\n\n```typescript\nimport { defineSchema } from \"@json-render/core\";\n\nexport const schema = defineSchema((s) => ({\n  spec: s.object({\n    root: s.object({\n      type: s.ref(\"catalog.components\"),\n      props: s.propsOf(\"catalog.components\"),\n      children: s.array(s.string()), // Element keys (flat spec format)\n    }),\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      description: s.string(),\n    }),\n    actions: s.map({\n      description: s.string(),\n    }),\n  }),\n}), {\n  promptTemplate: myPromptTemplate, // Optional custom AI prompt generator\n});\n```\n\n### Create 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    Card: {\n      props: z.object({\n        title: z.string(),\n        subtitle: z.string().nullable(),\n      }),\n      description: \"A card container with title\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\"]).nullable(),\n      }),\n      description: \"A clickable button\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit the form\" },\n    cancel: { description: \"Cancel and close\" },\n  },\n});\n```\n\n### Generate AI Prompts\n\n```typescript\n// Generate system prompt for AI\nconst systemPrompt = catalog.prompt();\n\n// With custom rules\nconst systemPrompt = catalog.prompt({\n  system: \"You are a dashboard builder.\",\n  customRules: [\n    \"Always include a header\",\n    \"Use Card components for grouping\",\n  ],\n});\n```\n\n### Stream AI Responses (SpecStream)\n\nThe SpecStream format uses JSONL patches to progressively build specs:\n\n```typescript\nimport { createSpecStreamCompiler } from \"@json-render/core\";\n\n// Create a compiler for your spec type\nconst compiler = createSpecStreamCompiler<MySpec>();\n\n// Process streaming chunks from AI\nwhile (streaming) {\n  const chunk = await reader.read();\n  const { result, newPatches } = compiler.push(chunk);\n  \n  if (newPatches.length > 0) {\n    // Update UI with partial result\n    setSpec(result);\n  }\n}\n\n// Get final compiled result\nconst finalSpec = compiler.getResult();\n```\n\nSpecStream format uses [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations (each line is a patch):\n\n```jsonl\n{\"op\":\"add\",\"path\":\"/root\",\"value\":\"card-1\"}\n{\"op\":\"add\",\"path\":\"/elements/card-1\",\"value\":{\"type\":\"Card\",\"props\":{\"title\":\"Hello\"},\"children\":[\"btn-1\"]}}\n{\"op\":\"add\",\"path\":\"/elements/btn-1\",\"value\":{\"type\":\"Button\",\"props\":{\"label\":\"Click\"},\"children\":[]}}\n```\n\nAll six RFC 6902 operations are supported: `add`, `remove`, `replace`, `move`, `copy`, `test`.\n\n### Low-Level Utilities\n\n```typescript\nimport {\n  parseSpecStreamLine,\n  applySpecStreamPatch,\n  compileSpecStream,\n} from \"@json-render/core\";\n\n// Parse a single line\nconst patch = parseSpecStreamLine('{\"op\":\"add\",\"path\":\"/root\",\"value\":{}}');\n// { op: \"add\", path: \"/root\", value: {} }\n\n// Apply a patch to an object\nconst obj = {};\napplySpecStreamPatch(obj, patch);\n// obj is now { root: {} }\n\n// Compile entire JSONL string at once\nconst spec = compileSpecStream<MySpec>(jsonlString);\n```\n\n## API Reference\n\n### Schema\n\n| Export | Purpose |\n|--------|---------|\n| `defineSchema(builder, options?)` | Create a schema with spec/catalog structure |\n| `SchemaBuilder` | Builder with `s.object()`, `s.array()`, `s.map()`, etc. |\n\nSchema options:\n\n| Option | Purpose |\n|--------|---------|\n| `promptTemplate` | Custom AI prompt generator |\n| `defaultRules` | Default rules injected before custom rules in prompts |\n| `builtInActions` | Actions always available at runtime, auto-injected into prompts (e.g. `setState`) |\n\n### Catalog\n\n| Export | Purpose |\n|--------|---------|\n| `defineCatalog(schema, data)` | Create a type-safe catalog from schema |\n| `catalog.prompt(options?)` | Generate AI system prompt |\n\n### SpecStream\n\n| Export | Purpose |\n|--------|---------|\n| `createSpecStreamCompiler<T>()` | Create streaming compiler |\n| `parseSpecStreamLine(line)` | Parse single JSONL line |\n| `applySpecStreamPatch(obj, patch)` | Apply patch to object |\n| `compileSpecStream<T>(jsonl)` | Compile entire JSONL string |\n\n### Dynamic Props\n\n| Export | Purpose |\n|--------|---------|\n| `resolvePropValue(value, ctx)` | Resolve a single prop expression |\n| `resolveElementProps(props, ctx)` | Resolve all prop expressions in an element |\n| `PropExpression<T>` | Type for prop values that may contain expressions |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `PropResolutionContext` | Context for resolving props (includes `functions` for `$computed`) |\n\n### Validation\n\n| Export | Purpose |\n|--------|---------|\n| `check.required()` | Required validation helper |\n| `check.email()` | Email validation helper |\n| `check.matches(path)` | Cross-field match helper |\n| `check.equalTo(path)` | Cross-field equality helper |\n| `check.lessThan(path)` | Cross-field less-than helper |\n| `check.greaterThan(path)` | Cross-field greater-than helper |\n| `check.requiredIf(path)` | Conditional required helper |\n| `builtInValidationFunctions` | All built-in validation functions |\n| `runValidationCheck()` | Run a single validation check |\n\n### User Prompt\n\n| Export | Purpose |\n|--------|---------|\n| `buildUserPrompt(options)` | Build a user prompt with optional spec refinement and state context |\n| `buildEditUserPrompt(options)` | Build a user prompt for editing an existing spec (used internally by `buildUserPrompt`) |\n| `buildEditInstructions(config, format)` | Generate the prompt section describing available edit modes |\n| `isNonEmptySpec(spec)` | Check whether a spec has a root and at least one element |\n| `UserPromptOptions` | Options type for `buildUserPrompt` |\n| `EditMode` | `\"patch\" \\| \"merge\" \\| \"diff\"` |\n| `EditConfig` | Configuration for edit modes (`{ modes: EditMode[] }`) |\n| `BuildEditUserPromptOptions` | Options type for `buildEditUserPrompt` |\n\n### Merge and Diff\n\n| Export | Purpose |\n|--------|---------|\n| `deepMergeSpec(base, patch)` | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |\n| `diffToPatches(oldObj, newObj)` | Generate RFC 6902 JSON Patch operations from object diff |\n\n### Spec Validation\n\n| Export | Purpose |\n|--------|---------|\n| `validateSpec(spec, options?)` | Validate spec structure and return issues |\n| `autoFixSpec(spec)` | Auto-fix common spec issues (returns corrected copy) |\n| `formatSpecIssues(issues)` | Format validation issues as readable strings |\n\n### Actions\n\n| Export | Purpose |\n|--------|---------|\n| `ActionBinding` | Action binding with `action`, `params`, `confirm`, `preventDefault`, etc. |\n| `BuiltInAction` | Built-in action definition with `name` and `description` |\n\n### Inline Mode (Mixed Streams)\n\n| Export | Purpose |\n|--------|---------|\n| `createJsonRenderTransform()` | TransformStream that separates text from JSONL patches in a mixed stream |\n| `pipeJsonRender()` | Server-side helper to pipe a mixed stream through the transform |\n| `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` | Constants for filtering spec data parts |\n\nThe transform splits text blocks around spec data by emitting `text-end`/`text-start` pairs, ensuring the AI SDK creates separate text parts and preserving correct interleaving of prose and UI in `message.parts`.\n\n### State Store\n\n| Export | Purpose |\n|--------|---------|\n| `createStateStore(initialState?)` | Create a framework-agnostic in-memory `StateStore` |\n| `StateStore` | Interface for plugging in external state management (Redux, Zustand, XState, etc.) |\n| `StateModel` | State model type (`Record<string, unknown>`) |\n\nThe `StateStore` interface allows renderers to use external state management instead of the built-in internal store:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/core\";\n\n// Simple in-memory store\nconst store = createStateStore({ count: 0 });\n\nstore.get(\"/count\");          // 0\nstore.set(\"/count\", 1);       // updates and notifies subscribers\nstore.getSnapshot();          // { count: 1 }\n\n// Subscribe to changes (compatible with React's useSyncExternalStore)\nconst unsubscribe = store.subscribe(() => {\n  console.log(\"state changed:\", store.getSnapshot());\n});\n```\n\nPass the store to `StateProvider` in any renderer package (`@json-render/react`, `@json-render/react-native`, `@json-render/react-pdf`) for controlled mode.\n\n### Store Utilities (for adapter authors)\n\nAvailable via `@json-render/core/store-utils`:\n\n| Export | Purpose |\n|--------|---------|\n| `createStoreAdapter(config)` | Build a full `StateStore` from a minimal `{ getSnapshot, setSnapshot, subscribe }` config |\n| `immutableSetByPath(root, path, value)` | Immutably set a value at a JSON Pointer path with structural sharing |\n| `flattenToPointers(obj)` | Flatten a nested object into JSON Pointer keyed entries |\n| `StoreAdapterConfig` | Config type for `createStoreAdapter` |\n\n```typescript\nimport { createStoreAdapter, immutableSetByPath, flattenToPointers } from \"@json-render/core/store-utils\";\n```\n\n`createStoreAdapter` handles `get`, `set` (with no-op detection), batched `update`, `getSnapshot`, `getServerSnapshot`, and `subscribe` -- adapter authors only need to supply the snapshot source, write API, and subscribe mechanism:\n\n```typescript\nimport { createStoreAdapter } from \"@json-render/core/store-utils\";\n\nconst store = createStoreAdapter({\n  getSnapshot: () => myLib.getState(),\n  setSnapshot: (next) => myLib.setState(next),\n  subscribe: (listener) => myLib.subscribe(listener),\n});\n```\n\nThe official adapter packages (`@json-render/redux`, `@json-render/zustand`, `@json-render/jotai`) are all built on top of `createStoreAdapter`.\n\n### Types\n\n| Export | Purpose |\n|--------|---------|\n| `Spec` | Base spec type |\n| `Catalog` | Catalog type |\n| `BuiltInAction` | Built-in action type (`name` + `description`) |\n| `ComputedFunction` | Function signature for `$computed` expressions |\n| `VisibilityCondition` | Visibility condition type (used by `$cond`) |\n| `VisibilityContext` | Context for evaluating visibility and prop expressions |\n| `SpecStreamLine` | Single patch operation |\n| `SpecStreamCompiler` | Streaming compiler interface |\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression that resolves based on data state at render time. Expressions are resolved by the renderer before props reach components.\n\n### Data Binding (`$state`)\n\nRead a value directly from the state model:\n\n```json\n{\n  \"color\": { \"$state\": \"/theme/primary\" },\n  \"label\": { \"$state\": \"/user/name\" }\n}\n```\n\n### Two-Way Binding (`$bindState` / `$bindItem`)\n\nUse `{ \"$bindState\": \"/path\" }` on the natural value prop for form components that need read/write access. The component reads from and writes to the state path:\n\n```json\n{\n  \"type\": \"Input\",\n  \"props\": {\n    \"value\": { \"$bindState\": \"/form/email\" },\n    \"placeholder\": \"Email\"\n  }\n}\n```\n\nInside a repeat scope, use `{ \"$bindItem\": \"completed\" }` to bind to a field on the current item:\n\n### Conditional (`$cond` / `$then` / `$else`)\n\nEvaluate a condition (same syntax as visibility conditions) and pick a value:\n\n```json\n{\n  \"color\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"#007AFF\",\n    \"$else\": \"#8E8E93\"\n  },\n  \"name\": {\n    \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n    \"$then\": \"home\",\n    \"$else\": \"home-outline\"\n  }\n}\n```\n\n`$then` and `$else` can themselves be expressions (recursive):\n\n```json\n{\n  \"label\": {\n    \"$cond\": { \"$state\": \"/user/isAdmin\" },\n    \"$then\": { \"$state\": \"/admin/greeting\" },\n    \"$else\": \"Welcome\"\n  }\n}\n```\n\n### Repeat Item (`$item`)\n\nInside children of a repeated element, read a field from the current array item:\n\n```json\n{ \"$item\": \"title\" }\n```\n\nUse `\"\"` to get the entire item object. `$item` takes a path string because items are typically objects with nested fields to navigate.\n\n### Repeat Index (`$index`)\n\nGet the current array index inside a repeat:\n\n```json\n{ \"$index\": true }\n```\n\n`$index` uses `true` as a sentinel flag because the index is a scalar value with no sub-path to navigate (unlike `$item` which needs a path).\n\n### Template (`$template`)\n\nInterpolate state values into strings using `${/path}` syntax:\n\n```json\n{\n  \"label\": { \"$template\": \"Hello, ${/user/name}! You have ${/inbox/count} messages.\" }\n}\n```\n\nMissing paths resolve to an empty string.\n\n### Computed (`$computed`)\n\nCall a registered function with resolved arguments:\n\n```json\n{\n  \"text\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\nFunctions are registered in the catalog and provided at runtime via the `functions` prop on the renderer.\n\n```typescript\nimport type { ComputedFunction } from \"@json-render/core\";\n\nconst functions: Record<string, ComputedFunction> = {\n  fullName: (args) => `${args.first} ${args.last}`,\n};\n```\n\n### API\n\n```typescript\nimport { resolvePropValue, resolveElementProps } from \"@json-render/core\";\n\n// Resolve a single value\nconst color = resolvePropValue(\n  { $cond: { $state: \"/active\", eq: \"yes\" }, $then: \"blue\", $else: \"gray\" },\n  { stateModel: myState }\n);\n\n// Resolve all props on an element\nconst resolved = resolveElementProps(element.props, { stateModel: myState });\n```\n\n## Visibility Conditions\n\nVisibility conditions control when elements are shown. `VisibilityContext` is `{ stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }`.\n\n### Syntax\n\n```typescript\n{ \"$state\": \"/path\" }                          // truthiness\n{ \"$state\": \"/path\", \"not\": true }             // falsy\n{ \"$state\": \"/path\", \"eq\": value }             // equality\n{ \"$state\": \"/path\", \"neq\": value }            // inequality\n{ \"$state\": \"/path\", \"gt\": number }            // greater than\n{ \"$item\": \"field\" }                          // repeat item field\n{ \"$index\": true, \"gt\": 0 }                   // repeat index\n[ condition, condition ]                       // implicit AND\n{ \"$and\": [ condition, condition ] }           // explicit AND\n{ \"$or\": [ condition, condition ] }            // OR\ntrue / false                                   // always / never\n```\n\n### TypeScript Helpers\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\nvisibility.always              // true\nvisibility.never               // false\nvisibility.when(\"/path\")       // { $state: \"/path\" }\nvisibility.unless(\"/path\")     // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)    // { $state: \"/path\", eq: val }\nvisibility.neq(\"/path\", val)   // { $state: \"/path\", neq: val }\nvisibility.gt(\"/path\", n)      // { $state: \"/path\", gt: n }\nvisibility.gte(\"/path\", n)     // { $state: \"/path\", gte: n }\nvisibility.lt(\"/path\", n)      // { $state: \"/path\", lt: n }\nvisibility.lte(\"/path\", n)     // { $state: \"/path\", lte: n }\nvisibility.and(cond1, cond2)   // { $and: [cond1, cond2] }\nvisibility.or(cond1, cond2)    // { $or: [cond1, cond2] }\n```\n\n## User Prompt Builder\n\nBuild structured user prompts for AI generation, with support for refinement and state context:\n\n```typescript\nimport { buildUserPrompt } from \"@json-render/core\";\n\n// Fresh generation\nconst prompt = buildUserPrompt({ prompt: \"create a todo app\" });\n\n// Refinement with edit modes (default: patch-only)\nconst refinementPrompt = buildUserPrompt({\n  prompt: \"add a dark mode toggle\",\n  currentSpec: existingSpec,\n  editModes: [\"patch\", \"merge\"],\n});\n\n// With runtime state context\nconst contextPrompt = buildUserPrompt({\n  prompt: \"show my data\",\n  state: { todos: [{ text: \"Buy milk\" }] },\n});\n```\n\nWhen `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes. Available modes:\n\n- **`\"patch\"`** — RFC 6902 JSON Patch. One operation per line. Best for precise, targeted single-field updates.\n- **`\"merge\"`** — RFC 7396 JSON Merge Patch. Partial object deep-merged; `null` deletes. Best for structural changes.\n- **`\"diff\"`** — Unified diff against the serialized spec. Best for small text-level changes.\n\n## Deep Merge and Diff\n\nFormat-agnostic utilities for working with specs:\n\n```typescript\nimport { deepMergeSpec, diffToPatches } from \"@json-render/core\";\n\n// RFC 7396 deep merge: null deletes, arrays replace, objects recurse\nconst merged = deepMergeSpec(baseSpec, { elements: { main: { props: { title: \"New\" } } } });\n\n// RFC 6902 diff: generate JSON Patch operations from two objects\nconst patches = diffToPatches(oldSpec, newSpec);\n// [{ op: \"replace\", path: \"/elements/main/props/title\", value: \"New\" }]\n```\n\n## Spec Validation\n\nValidate spec structure and auto-fix common issues:\n\n```typescript\nimport { validateSpec, autoFixSpec, formatSpecIssues } from \"@json-render/core\";\n\n// Validate a spec\nconst { valid, issues } = validateSpec(spec);\n\n// Format issues for display\nconsole.log(formatSpecIssues(issues));\n\n// Auto-fix common issues (returns a corrected copy)\nconst fixed = autoFixSpec(spec);\n```\n\n## State Watchers\n\nElements can declare a `watch` field to trigger actions when state values change. `watch` is a top-level field on the element (sibling of `type`, `props`, `children`), not inside `props`.\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"label\": \"Country\",\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\", \"UK\"]\n  },\n  \"watch\": {\n    \"/form/country\": {\n      \"action\": \"loadCities\",\n      \"params\": { \"country\": { \"$state\": \"/form/country\" } }\n    }\n  },\n  \"children\": []\n}\n```\n\nWatchers only fire on value changes, not on initial render. Multiple action bindings per path execute sequentially.\n\n## Validation\n\n### Built-in Validation Functions\n\n| Function | Description | Args |\n|----------|-------------|------|\n| `required` | Value must not be empty | — |\n| `email` | Must be a valid email | — |\n| `url` | Must be a valid URL | — |\n| `numeric` | Must be a number | — |\n| `minLength` | Minimum string length | `{ min: number }` |\n| `maxLength` | Maximum string length | `{ max: number }` |\n| `min` | Minimum numeric value | `{ min: number }` |\n| `max` | Maximum numeric value | `{ max: number }` |\n| `pattern` | Must match regex | `{ pattern: string }` |\n| `matches` | Must equal another field | `{ other: { $state: \"/path\" } }` |\n| `equalTo` | Alias for matches | `{ other: { $state: \"/path\" } }` |\n| `lessThan` | Must be less than another field | `{ other: { $state: \"/path\" } }` |\n| `greaterThan` | Must be greater than another field | `{ other: { $state: \"/path\" } }` |\n| `requiredIf` | Required when condition is truthy | `{ field: { $state: \"/path\" } }` |\n\n### TypeScript Helpers\n\n```typescript\nimport { check } from \"@json-render/core\";\n\ncheck.required(\"Field is required\");\ncheck.email(\"Invalid email\");\ncheck.matches(\"/form/password\", \"Passwords must match\");\ncheck.equalTo(\"/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 notifications enabled\");\n```\n\n## Custom Schemas\n\njson-render supports completely different spec formats for different renderers:\n\n```typescript\n// React: Flat element map\n{ root: \"card-1\", elements: { \"card-1\": { type: \"Card\", props: {...}, children: [...] } } }\n\n// Remotion: Timeline\n{ composition: {...}, tracks: [...], clips: [...] }\n\n// Your own: Whatever you need\n{ pages: [...], navigation: {...}, theme: {...} }\n```\n\nEach renderer defines its own schema with `defineSchema()` and its own prompt template.\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@json-render/core\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"JSON becomes real things. Define your catalog, register your components, let AI generate.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"react\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"schema\",\n    \"zod\",\n    \"streaming\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/core\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./store-utils\": {\n      \"types\": \"./dist/store-utils.d.ts\",\n      \"import\": \"./dist/store-utils.mjs\",\n      \"require\": \"./dist/store-utils.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  },\n  \"peerDependencies\": {\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/core/src/actions.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport {\n  resolveAction,\n  executeAction,\n  interpolateString,\n  actionBinding,\n} from \"./actions\";\n\ndescribe(\"interpolateString\", () => {\n  it(\"interpolates ${path} expressions\", () => {\n    const data = { user: { name: \"Alice\" }, count: 5 };\n\n    expect(interpolateString(\"Hello ${/user/name}!\", data)).toBe(\n      \"Hello Alice!\",\n    );\n    expect(interpolateString(\"${/user/name} has ${/count} items\", data)).toBe(\n      \"Alice has 5 items\",\n    );\n  });\n\n  it(\"returns string unchanged when no variables\", () => {\n    expect(interpolateString(\"No vars here\", {})).toBe(\"No vars here\");\n  });\n\n  it(\"replaces missing values with empty string\", () => {\n    expect(interpolateString(\"Hello ${/missing}!\", {})).toBe(\"Hello !\");\n  });\n\n  it(\"handles multiple occurrences of same variable\", () => {\n    const data = { name: \"Bob\" };\n    expect(interpolateString(\"${/name} says ${/name}\", data)).toBe(\n      \"Bob says Bob\",\n    );\n  });\n});\n\ndescribe(\"resolveAction\", () => {\n  it(\"resolves literal params\", () => {\n    const resolved = resolveAction(\n      {\n        action: \"navigate\",\n        params: { url: \"/home\", count: 5 },\n      },\n      {},\n    );\n\n    expect(resolved.action).toBe(\"navigate\");\n    expect(resolved.params.url).toBe(\"/home\");\n    expect(resolved.params.count).toBe(5);\n  });\n\n  it(\"resolves dynamic $state params\", () => {\n    const data = { userId: 123, settings: { theme: \"dark\" } };\n    const resolved = resolveAction(\n      {\n        action: \"updateUser\",\n        params: {\n          id: { $state: \"/userId\" },\n          theme: { $state: \"/settings/theme\" },\n        },\n      },\n      data,\n    );\n\n    expect(resolved.params.id).toBe(123);\n    expect(resolved.params.theme).toBe(\"dark\");\n  });\n\n  it(\"interpolates confirmation messages\", () => {\n    const data = { user: { name: \"Alice\" } };\n    const resolved = resolveAction(\n      {\n        action: \"delete\",\n        confirm: {\n          title: \"Delete ${/user/name}\",\n          message: \"Are you sure you want to delete ${/user/name}?\",\n        },\n      },\n      data,\n    );\n\n    expect(resolved.confirm?.title).toBe(\"Delete Alice\");\n    expect(resolved.confirm?.message).toBe(\n      \"Are you sure you want to delete Alice?\",\n    );\n  });\n\n  it(\"preserves onSuccess and onError handlers\", () => {\n    const resolved = resolveAction(\n      {\n        action: \"save\",\n        onSuccess: { navigate: \"/success\" },\n        onError: { set: { error: \"$error.message\" } },\n      },\n      {},\n    );\n\n    expect(resolved.onSuccess).toEqual({ navigate: \"/success\" });\n    expect(resolved.onError).toEqual({ set: { error: \"$error.message\" } });\n  });\n});\n\ndescribe(\"executeAction\", () => {\n  it(\"calls the handler with resolved params\", async () => {\n    const handler = vi.fn().mockResolvedValue(undefined);\n\n    await executeAction({\n      action: { action: \"test\", params: { value: 42 } },\n      handler,\n      setState: vi.fn(),\n    });\n\n    expect(handler).toHaveBeenCalledWith({ value: 42 });\n  });\n\n  it(\"handles onSuccess with navigate\", async () => {\n    const navigate = vi.fn();\n\n    await executeAction({\n      action: {\n        action: \"test\",\n        params: {},\n        onSuccess: { navigate: \"/success\" },\n      },\n      handler: vi.fn().mockResolvedValue(undefined),\n      setState: vi.fn(),\n      navigate,\n    });\n\n    expect(navigate).toHaveBeenCalledWith(\"/success\");\n  });\n\n  it(\"handles onSuccess with set\", async () => {\n    const setState = vi.fn();\n\n    await executeAction({\n      action: {\n        action: \"test\",\n        params: {},\n        onSuccess: { set: { saved: true, message: \"Done\" } },\n      },\n      handler: vi.fn().mockResolvedValue(undefined),\n      setState,\n    });\n\n    expect(setState).toHaveBeenCalledWith(\"saved\", true);\n    expect(setState).toHaveBeenCalledWith(\"message\", \"Done\");\n  });\n\n  it(\"handles onSuccess with action\", async () => {\n    const executeActionFn = vi.fn();\n\n    await executeAction({\n      action: {\n        action: \"test\",\n        params: {},\n        onSuccess: { action: \"followUp\" },\n      },\n      handler: vi.fn().mockResolvedValue(undefined),\n      setState: vi.fn(),\n      executeAction: executeActionFn,\n    });\n\n    expect(executeActionFn).toHaveBeenCalledWith(\"followUp\");\n  });\n\n  it(\"handles onError with set\", async () => {\n    const setState = vi.fn();\n    const error = new Error(\"Something went wrong\");\n\n    await executeAction({\n      action: {\n        action: \"test\",\n        params: {},\n        onError: { set: { error: \"$error.message\" } },\n      },\n      handler: vi.fn().mockRejectedValue(error),\n      setState,\n    });\n\n    expect(setState).toHaveBeenCalledWith(\"error\", \"Something went wrong\");\n  });\n\n  it(\"handles onError with action\", async () => {\n    const executeActionFn = vi.fn();\n    const error = new Error(\"Failed\");\n\n    await executeAction({\n      action: {\n        action: \"test\",\n        params: {},\n        onError: { action: \"handleError\" },\n      },\n      handler: vi.fn().mockRejectedValue(error),\n      setState: vi.fn(),\n      executeAction: executeActionFn,\n    });\n\n    expect(executeActionFn).toHaveBeenCalledWith(\"handleError\");\n  });\n\n  it(\"re-throws error when no onError handler\", async () => {\n    const error = new Error(\"Unhandled\");\n\n    await expect(\n      executeAction({\n        action: { action: \"test\", params: {} },\n        handler: vi.fn().mockRejectedValue(error),\n        setState: vi.fn(),\n      }),\n    ).rejects.toThrow(\"Unhandled\");\n  });\n});\n\ndescribe(\"actionBinding helper\", () => {\n  describe(\"simple\", () => {\n    it(\"creates binding with action only\", () => {\n      const a = actionBinding.simple(\"navigate\");\n\n      expect(a.action).toBe(\"navigate\");\n      expect(a.params).toBeUndefined();\n    });\n\n    it(\"creates binding with params\", () => {\n      const a = actionBinding.simple(\"navigate\", { url: \"/home\" });\n\n      expect(a.action).toBe(\"navigate\");\n      expect(a.params).toEqual({ url: \"/home\" });\n    });\n  });\n\n  describe(\"withConfirm\", () => {\n    it(\"creates binding with confirmation\", () => {\n      const a = actionBinding.withConfirm(\"delete\", {\n        title: \"Confirm\",\n        message: \"Are you sure?\",\n      });\n\n      expect(a.action).toBe(\"delete\");\n      expect(a.confirm).toEqual({\n        title: \"Confirm\",\n        message: \"Are you sure?\",\n      });\n    });\n\n    it(\"creates binding with confirmation and params\", () => {\n      const a = actionBinding.withConfirm(\n        \"delete\",\n        { title: \"Delete\", message: \"Delete item?\" },\n        { id: 123 },\n      );\n\n      expect(a.action).toBe(\"delete\");\n      expect(a.params).toEqual({ id: 123 });\n      expect(a.confirm?.title).toBe(\"Delete\");\n    });\n  });\n\n  describe(\"withSuccess\", () => {\n    it(\"creates binding with navigate success handler\", () => {\n      const a = actionBinding.withSuccess(\"save\", { navigate: \"/success\" });\n\n      expect(a.action).toBe(\"save\");\n      expect(a.onSuccess).toEqual({ navigate: \"/success\" });\n    });\n\n    it(\"creates binding with set success handler\", () => {\n      const a = actionBinding.withSuccess(\"save\", { set: { saved: true } });\n\n      expect(a.onSuccess).toEqual({ set: { saved: true } });\n    });\n\n    it(\"creates binding with success handler and params\", () => {\n      const a = actionBinding.withSuccess(\n        \"save\",\n        { navigate: \"/done\" },\n        { data: \"test\" },\n      );\n\n      expect(a.params).toEqual({ data: \"test\" });\n      expect(a.onSuccess).toEqual({ navigate: \"/done\" });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/actions.ts",
    "content": "import { z } from \"zod\";\nimport type { DynamicValue, StateModel } from \"./types\";\nimport { DynamicValueSchema, resolveDynamicValue } from \"./types\";\n\n/**\n * Confirmation dialog configuration\n */\nexport interface ActionConfirm {\n  title: string;\n  message: string;\n  confirmLabel?: string;\n  cancelLabel?: string;\n  variant?: \"default\" | \"danger\";\n}\n\n/**\n * Action success handler\n */\nexport type ActionOnSuccess =\n  | { navigate: string }\n  | { set: Record<string, unknown> }\n  | { action: string };\n\n/**\n * Action error handler\n */\nexport type ActionOnError =\n  | { set: Record<string, unknown> }\n  | { action: string };\n\n/**\n * Action binding — maps an event to an action invocation.\n *\n * Used inside the `on` field of a UIElement:\n * ```json\n * { \"on\": { \"press\": { \"action\": \"setState\", \"params\": { \"statePath\": \"/x\", \"value\": 1 } } } }\n * ```\n */\nexport interface ActionBinding {\n  /** Action name (must be in catalog) */\n  action: string;\n  /** Parameters to pass to the action handler */\n  params?: Record<string, DynamicValue>;\n  /** Confirmation dialog before execution */\n  confirm?: ActionConfirm;\n  /** Handler after successful execution */\n  onSuccess?: ActionOnSuccess;\n  /** Handler after failed execution */\n  onError?: ActionOnError;\n  /** Whether to prevent default browser behavior (e.g. navigation on links) */\n  preventDefault?: boolean;\n}\n\n/**\n * @deprecated Use ActionBinding instead\n */\nexport type Action = ActionBinding;\n\n/**\n * Schema for action confirmation\n */\nexport const ActionConfirmSchema = z.object({\n  title: z.string(),\n  message: z.string(),\n  confirmLabel: z.string().optional(),\n  cancelLabel: z.string().optional(),\n  variant: z.enum([\"default\", \"danger\"]).optional(),\n});\n\n/**\n * Schema for success handlers\n */\nexport const ActionOnSuccessSchema = z.union([\n  z.object({ navigate: z.string() }),\n  z.object({ set: z.record(z.string(), z.unknown()) }),\n  z.object({ action: z.string() }),\n]);\n\n/**\n * Schema for error handlers\n */\nexport const ActionOnErrorSchema = z.union([\n  z.object({ set: z.record(z.string(), z.unknown()) }),\n  z.object({ action: z.string() }),\n]);\n\n/**\n * Full action binding schema\n */\nexport const ActionBindingSchema = z.object({\n  action: z.string(),\n  params: z.record(z.string(), DynamicValueSchema).optional(),\n  confirm: ActionConfirmSchema.optional(),\n  onSuccess: ActionOnSuccessSchema.optional(),\n  onError: ActionOnErrorSchema.optional(),\n  preventDefault: z.boolean().optional(),\n});\n\n/**\n * @deprecated Use ActionBindingSchema instead\n */\nexport const ActionSchema = ActionBindingSchema;\n\n/**\n * Action handler function signature\n */\nexport type ActionHandler<\n  TParams = Record<string, unknown>,\n  TResult = unknown,\n> = (params: TParams) => Promise<TResult> | TResult;\n\n/**\n * Action definition in catalog\n */\nexport interface ActionDefinition<TParams = Record<string, unknown>> {\n  /** Zod schema for params validation */\n  params?: z.ZodType<TParams>;\n  /** Description for AI */\n  description?: string;\n}\n\n/**\n * Resolved action with all dynamic values resolved\n */\nexport interface ResolvedAction {\n  action: string;\n  params: Record<string, unknown>;\n  confirm?: ActionConfirm;\n  onSuccess?: ActionOnSuccess;\n  onError?: ActionOnError;\n}\n\n/**\n * Resolve all dynamic values in an action binding\n */\nexport function resolveAction(\n  binding: ActionBinding,\n  stateModel: StateModel,\n): ResolvedAction {\n  const resolvedParams: Record<string, unknown> = {};\n\n  if (binding.params) {\n    for (const [key, value] of Object.entries(binding.params)) {\n      resolvedParams[key] = resolveDynamicValue(value, stateModel);\n    }\n  }\n\n  // Interpolate confirmation message if present\n  let confirm = binding.confirm;\n  if (confirm) {\n    confirm = {\n      ...confirm,\n      message: interpolateString(confirm.message, stateModel),\n      title: interpolateString(confirm.title, stateModel),\n    };\n  }\n\n  return {\n    action: binding.action,\n    params: resolvedParams,\n    confirm,\n    onSuccess: binding.onSuccess,\n    onError: binding.onError,\n  };\n}\n\n/**\n * Interpolate ${path} expressions in a string\n */\nexport function interpolateString(\n  template: string,\n  stateModel: StateModel,\n): string {\n  return template.replace(/\\$\\{([^}]+)\\}/g, (_, path) => {\n    const value = resolveDynamicValue({ $state: path }, stateModel);\n    return String(value ?? \"\");\n  });\n}\n\n/**\n * Context for action execution\n */\nexport interface ActionExecutionContext {\n  /** The resolved action */\n  action: ResolvedAction;\n  /** The action handler from the host */\n  handler: ActionHandler;\n  /** Function to update state model */\n  setState: (path: string, value: unknown) => void;\n  /** Function to navigate */\n  navigate?: (path: string) => void;\n  /** Function to execute another action */\n  executeAction?: (name: string) => Promise<void>;\n}\n\n/**\n * Execute an action with all callbacks\n */\nexport async function executeAction(\n  ctx: ActionExecutionContext,\n): Promise<void> {\n  const { action, handler, setState, navigate, executeAction } = ctx;\n\n  try {\n    await handler(action.params);\n\n    // Handle success\n    if (action.onSuccess) {\n      if (\"navigate\" in action.onSuccess && navigate) {\n        navigate(action.onSuccess.navigate);\n      } else if (\"set\" in action.onSuccess) {\n        for (const [path, value] of Object.entries(action.onSuccess.set)) {\n          setState(path, value);\n        }\n      } else if (\"action\" in action.onSuccess && executeAction) {\n        await executeAction(action.onSuccess.action);\n      }\n    }\n  } catch (error) {\n    // Handle error\n    if (action.onError) {\n      if (\"set\" in action.onError) {\n        for (const [path, value] of Object.entries(action.onError.set)) {\n          // Replace $error.message with actual error\n          const resolvedValue =\n            typeof value === \"string\" && value === \"$error.message\"\n              ? (error as Error).message\n              : value;\n          setState(path, resolvedValue);\n        }\n      } else if (\"action\" in action.onError && executeAction) {\n        await executeAction(action.onError.action);\n      }\n    } else {\n      throw error;\n    }\n  }\n}\n\n/**\n * Helper to create action bindings\n */\nexport const actionBinding = {\n  /** Create a simple action binding */\n  simple: (\n    actionName: string,\n    params?: Record<string, DynamicValue>,\n  ): ActionBinding => ({\n    action: actionName,\n    params,\n  }),\n\n  /** Create an action binding with confirmation */\n  withConfirm: (\n    actionName: string,\n    confirm: ActionConfirm,\n    params?: Record<string, DynamicValue>,\n  ): ActionBinding => ({\n    action: actionName,\n    params,\n    confirm,\n  }),\n\n  /** Create an action binding with success handler */\n  withSuccess: (\n    actionName: string,\n    onSuccess: ActionOnSuccess,\n    params?: Record<string, DynamicValue>,\n  ): ActionBinding => ({\n    action: actionName,\n    params,\n    onSuccess,\n  }),\n};\n\n/**\n * @deprecated Use actionBinding instead\n */\nexport const action = actionBinding;\n"
  },
  {
    "path": "packages/core/src/diff.ts",
    "content": "import type { JsonPatch } from \"./types\";\n\n/**\n * Escape a single JSON Pointer token per RFC 6901.\n * `~` → `~0`, `/` → `~1`.\n */\nfunction escapeToken(token: string): string {\n  return token.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\n\nfunction buildPath(basePath: string, key: string): string {\n  return `${basePath}/${escapeToken(key)}`;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Shallow equality for arrays — used to avoid emitting patches when the\n * children list hasn't actually changed.\n */\nfunction arraysEqual(a: unknown[], b: unknown[]): boolean {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) return false;\n  }\n  return true;\n}\n\n/**\n * Produce RFC 6902 JSON Patch operations that transform `oldObj` into `newObj`.\n *\n * - New keys → `add`\n * - Changed scalar/array values → `replace`\n * - Removed keys → `remove`\n * - Arrays are compared shallowly and replaced atomically (not element-diffed)\n * - Plain objects recurse\n */\nexport function diffToPatches(\n  oldObj: Record<string, unknown>,\n  newObj: Record<string, unknown>,\n  basePath = \"\",\n): JsonPatch[] {\n  const patches: JsonPatch[] = [];\n\n  // Keys present in newObj\n  for (const key of Object.keys(newObj)) {\n    const path = buildPath(basePath, key);\n    const oldVal = oldObj[key];\n    const newVal = newObj[key];\n\n    if (!(key in oldObj)) {\n      patches.push({ op: \"add\", path, value: newVal });\n      continue;\n    }\n\n    // Both exist — compare\n    if (isPlainObject(oldVal) && isPlainObject(newVal)) {\n      patches.push(...diffToPatches(oldVal, newVal, path));\n    } else if (Array.isArray(oldVal) && Array.isArray(newVal)) {\n      if (!arraysEqual(oldVal, newVal)) {\n        patches.push({ op: \"replace\", path, value: newVal });\n      }\n    } else if (oldVal !== newVal) {\n      patches.push({ op: \"replace\", path, value: newVal });\n    }\n  }\n\n  // Keys removed from oldObj\n  for (const key of Object.keys(oldObj)) {\n    if (!(key in newObj)) {\n      patches.push({ op: \"remove\", path: buildPath(basePath, key) });\n    }\n  }\n\n  return patches;\n}\n"
  },
  {
    "path": "packages/core/src/edit-modes.ts",
    "content": "import type { Spec } from \"./types\";\n\n/**\n * Edit mode for modifying an existing spec.\n *\n * - `\"patch\"` — RFC 6902 JSON Patch. One operation per line.\n * - `\"merge\"` — RFC 7396 JSON Merge Patch. Partial object deep-merged; `null` deletes.\n * - `\"diff\"`  — Unified diff (POSIX). Line-level text edits against the serialized spec.\n */\nexport type EditMode = \"patch\" | \"merge\" | \"diff\";\n\nexport interface EditConfig {\n  /** Which edit modes are enabled. When >1, the AI chooses per edit. */\n  modes: EditMode[];\n}\n\nconst DEFAULT_MODES: EditMode[] = [\"patch\"];\n\nfunction normalizeModes(config?: EditConfig): EditMode[] {\n  if (!config?.modes?.length) return DEFAULT_MODES;\n  return config.modes;\n}\n\n// ── JSON-format instructions ──\n\nfunction jsonPatchInstructions(): string {\n  return [\n    \"PATCH MODE (RFC 6902 JSON Patch):\",\n    \"Output one JSON object per line. Each line is a patch operation.\",\n    '- Add: {\"op\":\"add\",\"path\":\"/elements/new-key\",\"value\":{...}}',\n    '- Replace: {\"op\":\"replace\",\"path\":\"/elements/existing-key\",\"value\":{...}}',\n    '- Remove: {\"op\":\"remove\",\"path\":\"/elements/old-key\"}',\n    \"Only output patches for what needs to change.\",\n  ].join(\"\\n\");\n}\n\nfunction jsonMergeInstructions(): string {\n  return [\n    \"MERGE MODE (RFC 7396 JSON Merge Patch):\",\n    \"Output a single JSON object on one line with __json_edit set to true.\",\n    \"Include only the keys that changed. Unmentioned keys are preserved.\",\n    \"Set a key to null to delete it.\",\n    \"\",\n    \"Example (update a title and add an element):\",\n    '{\"__json_edit\":true,\"elements\":{\"main\":{\"props\":{\"title\":\"New Title\"}},\"new-el\":{\"type\":\"Card\",\"props\":{},\"children\":[]}}}',\n    \"\",\n    \"Example (delete an element):\",\n    '{\"__json_edit\":true,\"elements\":{\"old-widget\":null}}',\n  ].join(\"\\n\");\n}\n\nfunction jsonDiffInstructions(): string {\n  return [\n    \"DIFF MODE (unified diff):\",\n    \"Output a unified diff inside a ```diff code fence.\",\n    \"The diff applies against the JSON-serialized current spec.\",\n    \"\",\n    \"Example:\",\n    \"```diff\",\n    \"--- a/spec.json\",\n    \"+++ b/spec.json\",\n    \"@@ -3,1 +3,1 @@\",\n    '-      \"title\": \"Login\"',\n    '+      \"title\": \"Welcome Back\"',\n    \"```\",\n  ].join(\"\\n\");\n}\n\n// ── YAML-format instructions ──\n\nfunction yamlPatchInstructions(): string {\n  return [\n    \"PATCH MODE (RFC 6902 JSON Patch):\",\n    \"Output RFC 6902 JSON Patch lines inside a ```yaml-patch code fence.\",\n    \"Each line is one JSON patch operation.\",\n    \"\",\n    \"Example:\",\n    \"```yaml-patch\",\n    '{\"op\":\"replace\",\"path\":\"/elements/main/props/title\",\"value\":\"New Title\"}',\n    '{\"op\":\"add\",\"path\":\"/elements/new-el\",\"value\":{\"type\":\"Card\",\"props\":{},\"children\":[]}}',\n    \"```\",\n  ].join(\"\\n\");\n}\n\nfunction yamlMergeInstructions(): string {\n  return [\n    \"MERGE MODE (RFC 7396 JSON Merge Patch):\",\n    \"Output only the changed parts in a ```yaml-edit code fence.\",\n    \"Uses deep merge semantics: only keys you include are updated. Unmentioned elements and props are preserved.\",\n    \"Set a key to null to delete it.\",\n    \"\",\n    \"Example edit (update title, add a new element):\",\n    \"```yaml-edit\",\n    \"elements:\",\n    \"  main:\",\n    \"    props:\",\n    \"      title: Updated Title\",\n    \"  new-chart:\",\n    \"    type: Card\",\n    \"    props: {}\",\n    \"    children: []\",\n    \"```\",\n    \"\",\n    \"Example deletion:\",\n    \"```yaml-edit\",\n    \"elements:\",\n    \"  old-widget: null\",\n    \"```\",\n  ].join(\"\\n\");\n}\n\nfunction yamlDiffInstructions(): string {\n  return [\n    \"DIFF MODE (unified diff):\",\n    \"Output a unified diff inside a ```diff code fence.\",\n    \"The diff applies against the YAML-serialized current spec.\",\n    \"\",\n    \"Example:\",\n    \"```diff\",\n    \"--- a/spec.yaml\",\n    \"+++ b/spec.yaml\",\n    \"@@ -6,1 +6,1 @@\",\n    \"-      title: Login\",\n    \"+      title: Welcome Back\",\n    \"```\",\n  ].join(\"\\n\");\n}\n\n// ── Mode selection guidance ──\n\nfunction modeSelectionGuidance(modes: EditMode[]): string {\n  if (modes.length === 1) return \"\";\n  const parts = [\"Choose the best edit strategy for the requested change:\"];\n  if (modes.includes(\"patch\")) {\n    parts.push(\"- PATCH: best for precise, targeted single-field updates\");\n  }\n  if (modes.includes(\"merge\")) {\n    parts.push(\n      \"- MERGE: best for structural changes (add/remove elements, reparent children, update multiple props at once)\",\n    );\n  }\n  if (modes.includes(\"diff\")) {\n    parts.push(\n      \"- DIFF: best for small text-level changes when you can see the exact lines to change\",\n    );\n  }\n  return parts.join(\"\\n\");\n}\n\n/**\n * Generate the prompt section describing available edit modes.\n * Only documents the modes that are enabled.\n */\nexport function buildEditInstructions(\n  config: EditConfig | undefined,\n  format: \"json\" | \"yaml\",\n): string {\n  const modes = normalizeModes(config);\n  const sections: string[] = [];\n\n  sections.push(\"EDITING EXISTING SPECS:\");\n  sections.push(\"\");\n\n  const guidance = modeSelectionGuidance(modes);\n  if (guidance) {\n    sections.push(guidance);\n    sections.push(\"\");\n  }\n\n  for (const mode of modes) {\n    if (format === \"json\") {\n      switch (mode) {\n        case \"patch\":\n          sections.push(jsonPatchInstructions());\n          break;\n        case \"merge\":\n          sections.push(jsonMergeInstructions());\n          break;\n        case \"diff\":\n          sections.push(jsonDiffInstructions());\n          break;\n      }\n    } else {\n      switch (mode) {\n        case \"patch\":\n          sections.push(yamlPatchInstructions());\n          break;\n        case \"merge\":\n          sections.push(yamlMergeInstructions());\n          break;\n        case \"diff\":\n          sections.push(yamlDiffInstructions());\n          break;\n      }\n    }\n    sections.push(\"\");\n  }\n\n  return sections.join(\"\\n\");\n}\n\nfunction addLineNumbers(text: string): string {\n  const lines = text.split(\"\\n\");\n  const width = String(lines.length).length;\n  return lines\n    .map((line, i) => `${String(i + 1).padStart(width)}| ${line}`)\n    .join(\"\\n\");\n}\n\nexport function isNonEmptySpec(spec: unknown): spec is Spec {\n  if (!spec || typeof spec !== \"object\") return false;\n  const s = spec as Record<string, unknown>;\n  return (\n    typeof s.root === \"string\" &&\n    typeof s.elements === \"object\" &&\n    s.elements !== null &&\n    Object.keys(s.elements as object).length > 0\n  );\n}\n\nexport interface BuildEditUserPromptOptions {\n  prompt: string;\n  currentSpec?: Spec | null;\n  config?: EditConfig;\n  format: \"json\" | \"yaml\";\n  maxPromptLength?: number;\n  /** Serialise the spec. Defaults to JSON.stringify for json, must be provided for yaml. */\n  serializer?: (spec: Spec) => string;\n}\n\n/**\n * Generate the user prompt for edits, including the current spec\n * (with line numbers when diff mode is enabled) and mode instructions.\n */\nexport function buildEditUserPrompt(\n  options: BuildEditUserPromptOptions,\n): string {\n  const { prompt, currentSpec, config, format, maxPromptLength, serializer } =\n    options;\n\n  let userText = String(prompt || \"\");\n  if (maxPromptLength !== undefined && maxPromptLength > 0) {\n    userText = userText.slice(0, maxPromptLength);\n  }\n\n  if (!isNonEmptySpec(currentSpec)) {\n    return userText;\n  }\n\n  const modes = normalizeModes(config);\n  const showLineNumbers = modes.includes(\"diff\");\n\n  const serialize = serializer ?? ((s: Spec) => JSON.stringify(s, null, 2));\n  const specText = serialize(currentSpec);\n\n  const parts: string[] = [];\n\n  if (showLineNumbers) {\n    parts.push(\"CURRENT UI STATE (line numbers for reference):\");\n    parts.push(\"```\");\n    parts.push(addLineNumbers(specText));\n    parts.push(\"```\");\n  } else {\n    parts.push(\n      \"CURRENT UI STATE (already loaded, DO NOT recreate existing elements):\",\n    );\n    parts.push(\"```\");\n    parts.push(specText);\n    parts.push(\"```\");\n  }\n\n  parts.push(\"\");\n  parts.push(`USER REQUEST: ${userText}`);\n  parts.push(\"\");\n\n  if (modes.length === 1) {\n    const mode = modes[0]!;\n    switch (mode) {\n      case \"patch\":\n        parts.push(\n          format === \"yaml\"\n            ? \"Output ONLY the patches in a ```yaml-patch fence.\"\n            : \"Output ONLY the JSON Patch lines needed for the change.\",\n        );\n        break;\n      case \"merge\":\n        parts.push(\n          format === \"yaml\"\n            ? \"Output ONLY the changes in a ```yaml-edit fence. Include only keys that need to change.\"\n            : \"Output ONLY a single JSON merge line with __json_edit set to true. Include only keys that need to change.\",\n        );\n        break;\n      case \"diff\":\n        parts.push(\"Output ONLY the unified diff in a ```diff fence.\");\n        break;\n    }\n  } else {\n    const modeNames = modes.map((m) => {\n      switch (m) {\n        case \"patch\":\n          return format === \"yaml\" ? \"```yaml-patch fence\" : \"JSON Patch lines\";\n        case \"merge\":\n          return format === \"yaml\"\n            ? \"```yaml-edit fence\"\n            : \"JSON merge line (__json_edit)\";\n        case \"diff\":\n          return \"```diff fence\";\n      }\n    });\n    parts.push(\n      `Choose the best edit strategy and output using one of: ${modeNames.join(\", \")}`,\n    );\n  }\n\n  return parts.join(\"\\n\");\n}\n"
  },
  {
    "path": "packages/core/src/env.d.ts",
    "content": "// Minimal process.env typing for dev-only warnings.\n// Uses a namespaced interface so it merges cleanly with @types/node if present.\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    readonly NODE_ENV?: string;\n  }\n}\n\ndeclare const process: { readonly env: NodeJS.ProcessEnv };\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "// Types\nexport type {\n  DynamicValue,\n  DynamicString,\n  DynamicNumber,\n  DynamicBoolean,\n  UIElement,\n  FlatElement,\n  Spec,\n  VisibilityCondition,\n  StateCondition,\n  ItemCondition,\n  IndexCondition,\n  SingleCondition,\n  AndCondition,\n  OrCondition,\n  StateModel,\n  StateStore,\n  ComponentSchema,\n  ValidationMode,\n  PatchOp,\n  JsonPatch,\n  // SpecStream types\n  SpecStreamLine,\n  SpecStreamCompiler,\n  // Mixed stream types (chat + GenUI)\n  MixedStreamCallbacks,\n  MixedStreamParser,\n  // AI SDK stream transform\n  StreamChunk,\n  SpecDataPart,\n} from \"./types\";\n\nexport {\n  DynamicValueSchema,\n  DynamicStringSchema,\n  DynamicNumberSchema,\n  DynamicBooleanSchema,\n  resolveDynamicValue,\n  getByPath,\n  setByPath,\n  addByPath,\n  removeByPath,\n  findFormValue,\n  // SpecStream - streaming format for building specs (RFC 6902)\n  parseSpecStreamLine,\n  applySpecStreamPatch,\n  applySpecPatch,\n  nestedToFlat,\n  compileSpecStream,\n  createSpecStreamCompiler,\n  // Mixed stream parser (chat + GenUI)\n  createMixedStreamParser,\n  // AI SDK stream transform\n  createJsonRenderTransform,\n  pipeJsonRender,\n  SPEC_DATA_PART,\n  SPEC_DATA_PART_TYPE,\n} from \"./types\";\n\n// State Store\nexport type { StoreAdapterConfig } from \"./state-store\";\nexport { createStateStore } from \"./state-store\";\n\n// Visibility\nexport type { VisibilityContext } from \"./visibility\";\n\nexport {\n  VisibilityConditionSchema,\n  evaluateVisibility,\n  visibility,\n} from \"./visibility\";\n\n// Prop Expressions\nexport type {\n  PropExpression,\n  PropResolutionContext,\n  ComputedFunction,\n} from \"./props\";\n\nexport {\n  resolvePropValue,\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n} from \"./props\";\n\n// Actions\nexport type {\n  ActionBinding,\n  /** @deprecated Use ActionBinding instead */\n  Action,\n  ActionConfirm,\n  ActionOnSuccess,\n  ActionOnError,\n  ActionHandler,\n  ActionDefinition,\n  ResolvedAction,\n  ActionExecutionContext,\n} from \"./actions\";\n\nexport {\n  ActionBindingSchema,\n  /** @deprecated Use ActionBindingSchema instead */\n  ActionSchema,\n  ActionConfirmSchema,\n  ActionOnSuccessSchema,\n  ActionOnErrorSchema,\n  resolveAction,\n  executeAction,\n  interpolateString,\n  actionBinding,\n  /** @deprecated Use actionBinding instead */\n  action,\n} from \"./actions\";\n\n// Validation\nexport type {\n  ValidationCheck,\n  ValidationConfig,\n  ValidationFunction,\n  ValidationFunctionDefinition,\n  ValidationCheckResult,\n  ValidationResult,\n  ValidationContext,\n} from \"./validation\";\n\nexport {\n  ValidationCheckSchema,\n  ValidationConfigSchema,\n  builtInValidationFunctions,\n  runValidationCheck,\n  runValidation,\n  check,\n} from \"./validation\";\n\n// Spec Structural Validation\nexport type {\n  SpecIssueSeverity,\n  SpecIssue,\n  SpecValidationIssues,\n  ValidateSpecOptions,\n} from \"./spec-validator\";\n\nexport { validateSpec, autoFixSpec, formatSpecIssues } from \"./spec-validator\";\n\n// Schema — defines the grammar (how specs and catalogs are structured)\nexport type {\n  SchemaBuilder,\n  SchemaType,\n  SchemaDefinition,\n  Schema,\n  PromptTemplate,\n  SchemaOptions,\n  BuiltInAction,\n} from \"./schema\";\n\nexport { defineSchema } from \"./schema\";\n\n// Catalog — defines the vocabulary (what components and actions are available)\nexport type {\n  Catalog,\n  JsonSchemaOptions,\n  PromptOptions,\n  PromptContext,\n  SpecValidationResult,\n  InferCatalogInput,\n  InferSpec,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n} from \"./schema\";\n\nexport { defineCatalog } from \"./schema\";\n\n// User Prompt Builder\nexport type { UserPromptOptions } from \"./prompt\";\n\nexport { buildUserPrompt } from \"./prompt\";\n\n// Object diff & merge (format-agnostic)\nexport { deepMergeSpec } from \"./merge\";\nexport { diffToPatches } from \"./diff\";\n\n// Edit modes\nexport type {\n  EditMode,\n  EditConfig,\n  BuildEditUserPromptOptions,\n} from \"./edit-modes\";\nexport {\n  buildEditInstructions,\n  buildEditUserPrompt,\n  isNonEmptySpec,\n} from \"./edit-modes\";\n"
  },
  {
    "path": "packages/core/src/merge.ts",
    "content": "function isPlainObject(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Deep-merge `patch` into `base`, returning a new object.\n *\n * Semantics (RFC 7396 JSON Merge Patch):\n * - `null` values in `patch` delete the corresponding key from `base`\n * - Arrays in `patch` replace (not concat) the corresponding array in `base`\n * - Plain objects recurse\n * - All other values replace\n *\n * Neither `base` nor `patch` is mutated.\n */\nexport function deepMergeSpec(\n  base: Record<string, unknown>,\n  patch: Record<string, unknown>,\n): Record<string, unknown> {\n  const result: Record<string, unknown> = { ...base };\n\n  for (const key of Object.keys(patch)) {\n    const patchVal = patch[key];\n\n    // null → delete\n    if (patchVal === null) {\n      delete result[key];\n      continue;\n    }\n\n    const baseVal = result[key];\n\n    if (isPlainObject(patchVal) && isPlainObject(baseVal)) {\n      result[key] = deepMergeSpec(baseVal, patchVal);\n    } else {\n      result[key] = patchVal;\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "packages/core/src/prompt.ts",
    "content": "import type { Spec } from \"./types\";\nimport type { EditMode } from \"./edit-modes\";\nimport { buildEditUserPrompt, isNonEmptySpec } from \"./edit-modes\";\n\n/**\n * Options for building a user prompt.\n */\nexport interface UserPromptOptions {\n  /** The user's text prompt */\n  prompt: string;\n  /** Existing spec to refine (triggers patch-only mode) */\n  currentSpec?: Spec | null;\n  /** Runtime state context to include */\n  state?: Record<string, unknown> | null;\n  /** Maximum length for the user's text prompt (applied before wrapping) */\n  maxPromptLength?: number;\n  /** Edit modes to offer when refining an existing spec. Default: `[\"patch\"]`. */\n  editModes?: EditMode[];\n  /** Wire format. Default: `\"json\"`. */\n  format?: \"json\" | \"yaml\";\n  /** Serialise the spec for edits. Defaults to JSON.stringify for json, must be provided for yaml. */\n  serializer?: (spec: Spec) => string;\n}\n\n/**\n * Build a user prompt for AI generation.\n *\n * Handles common patterns that every consuming app needs:\n * - Truncating the user's prompt to a max length\n * - Including the current spec for refinement (edit mode)\n * - Including runtime state context\n *\n * @example\n * ```ts\n * // Fresh generation\n * buildUserPrompt({ prompt: \"create a todo app\" })\n *\n * // Refinement with existing spec\n * buildUserPrompt({ prompt: \"add a dark mode toggle\", currentSpec: spec })\n *\n * // With multiple edit modes\n * buildUserPrompt({ prompt: \"change title\", currentSpec: spec, editModes: [\"patch\", \"merge\"] })\n * ```\n */\nexport function buildUserPrompt(options: UserPromptOptions): string {\n  const {\n    prompt,\n    currentSpec,\n    state,\n    maxPromptLength,\n    editModes,\n    format,\n    serializer,\n  } = options;\n\n  // Sanitize and optionally truncate the user's text\n  let userText = String(prompt || \"\");\n  if (maxPromptLength !== undefined && maxPromptLength > 0) {\n    userText = userText.slice(0, maxPromptLength);\n  }\n\n  // --- Refinement mode: currentSpec is provided ---\n  if (isNonEmptySpec(currentSpec)) {\n    const editPrompt = buildEditUserPrompt({\n      prompt: userText,\n      currentSpec,\n      config: { modes: editModes ?? [\"patch\"] },\n      format: format ?? \"json\",\n      serializer,\n    });\n\n    // Append state context if provided\n    if (state && Object.keys(state).length > 0) {\n      return `${editPrompt}\\n\\nAVAILABLE STATE:\\n${JSON.stringify(state, null, 2)}`;\n    }\n\n    return editPrompt;\n  }\n\n  // --- Fresh generation mode ---\n  const parts: string[] = [userText];\n\n  if (state && Object.keys(state).length > 0) {\n    parts.push(`\\nAVAILABLE STATE:\\n${JSON.stringify(state, null, 2)}`);\n  }\n\n  if (format === \"yaml\") {\n    parts.push(\n      `\\nOutput the full spec in a \\`\\`\\`yaml-spec fence. Stream progressively — output elements one at a time.`,\n    );\n  } else {\n    parts.push(\n      `\\nRemember: Output /root first, then interleave /elements and /state patches so the UI fills in progressively as it streams. Output each state patch right after the elements that use it, one per array item.`,\n    );\n  }\n\n  return parts.join(\"\\n\");\n}\n"
  },
  {
    "path": "packages/core/src/props.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport {\n  resolvePropValue,\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  _resetWarnedComputedFns,\n  _resetWarnedTemplatePaths,\n} from \"./props\";\nimport type { PropResolutionContext } from \"./props\";\n\n// =============================================================================\n// resolvePropValue\n// =============================================================================\n\ndescribe(\"resolvePropValue\", () => {\n  describe(\"literals\", () => {\n    it(\"passes through strings\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue(\"hello\", ctx)).toBe(\"hello\");\n    });\n\n    it(\"passes through numbers\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue(42, ctx)).toBe(42);\n    });\n\n    it(\"passes through booleans\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue(true, ctx)).toBe(true);\n      expect(resolvePropValue(false, ctx)).toBe(false);\n    });\n\n    it(\"passes through null\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue(null, ctx)).toBeNull();\n    });\n\n    it(\"passes through undefined\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue(undefined, ctx)).toBeUndefined();\n    });\n  });\n\n  describe(\"$state expressions\", () => {\n    it(\"resolves a state path\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { user: { name: \"Alice\" } },\n      };\n      expect(resolvePropValue({ $state: \"/user/name\" }, ctx)).toBe(\"Alice\");\n    });\n\n    it(\"returns undefined for missing state path\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue({ $state: \"/missing\" }, ctx)).toBeUndefined();\n    });\n\n    it(\"resolves nested state path\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { a: { b: { c: 42 } } },\n      };\n      expect(resolvePropValue({ $state: \"/a/b/c\" }, ctx)).toBe(42);\n    });\n  });\n\n  describe(\"$item expressions\", () => {\n    it(\"resolves a field from the repeat item\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { title: \"Hello\", id: \"1\" },\n        repeatIndex: 0,\n      };\n      expect(resolvePropValue({ $item: \"title\" }, ctx)).toBe(\"Hello\");\n    });\n\n    it('resolves \"/\" to the whole item', () => {\n      const item = { title: \"Hello\", id: \"1\" };\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: item,\n        repeatIndex: 0,\n      };\n      expect(resolvePropValue({ $item: \"\" }, ctx)).toBe(item);\n    });\n\n    it(\"resolves nested field from item\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { user: { name: \"Bob\" } },\n        repeatIndex: 0,\n      };\n      expect(resolvePropValue({ $item: \"user/name\" }, ctx)).toBe(\"Bob\");\n    });\n\n    it(\"returns undefined when no repeat item in context\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue({ $item: \"title\" }, ctx)).toBeUndefined();\n    });\n\n    it(\"returns undefined for missing field on item\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { title: \"Hello\" },\n        repeatIndex: 0,\n      };\n      expect(resolvePropValue({ $item: \"missing\" }, ctx)).toBeUndefined();\n    });\n  });\n\n  describe(\"$index expressions\", () => {\n    it(\"returns the current repeat index\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { id: \"1\" },\n        repeatIndex: 3,\n      };\n      expect(resolvePropValue({ $index: true }, ctx)).toBe(3);\n    });\n\n    it(\"returns 0 for first item\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { id: \"1\" },\n        repeatIndex: 0,\n      };\n      expect(resolvePropValue({ $index: true }, ctx)).toBe(0);\n    });\n\n    it(\"returns undefined when no repeat index in context\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue({ $index: true }, ctx)).toBeUndefined();\n    });\n  });\n\n  describe(\"$cond/$then/$else expressions\", () => {\n    it(\"returns $then when condition is true\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { active: true },\n      };\n      expect(\n        resolvePropValue(\n          { $cond: { $state: \"/active\" }, $then: \"blue\", $else: \"gray\" },\n          ctx,\n        ),\n      ).toBe(\"blue\");\n    });\n\n    it(\"returns $else when condition is false\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { active: false },\n      };\n      expect(\n        resolvePropValue(\n          { $cond: { $state: \"/active\" }, $then: \"blue\", $else: \"gray\" },\n          ctx,\n        ),\n      ).toBe(\"gray\");\n    });\n\n    it(\"handles eq condition\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { tab: \"home\" },\n      };\n      expect(\n        resolvePropValue(\n          {\n            $cond: { $state: \"/tab\", eq: \"home\" },\n            $then: \"#007AFF\",\n            $else: \"#8E8E93\",\n          },\n          ctx,\n        ),\n      ).toBe(\"#007AFF\");\n    });\n\n    it(\"handles nested expression in $then/$else\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { isAdmin: true, admin: { greeting: \"Hello Admin\" } },\n      };\n      expect(\n        resolvePropValue(\n          {\n            $cond: { $state: \"/isAdmin\" },\n            $then: { $state: \"/admin/greeting\" },\n            $else: \"Welcome\",\n          },\n          ctx,\n        ),\n      ).toBe(\"Hello Admin\");\n    });\n\n    it(\"handles array condition (implicit AND)\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { isAdmin: true, feature: true },\n      };\n      expect(\n        resolvePropValue(\n          {\n            $cond: [{ $state: \"/isAdmin\" }, { $state: \"/feature\" }],\n            $then: \"yes\",\n            $else: \"no\",\n          },\n          ctx,\n        ),\n      ).toBe(\"yes\");\n    });\n  });\n\n  describe(\"nested objects and arrays\", () => {\n    it(\"resolves expressions inside plain objects\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { color: \"red\", size: 12 },\n      };\n      const result = resolvePropValue(\n        { fill: { $state: \"/color\" }, fontSize: { $state: \"/size\" } },\n        ctx,\n      );\n      expect(result).toEqual({ fill: \"red\", fontSize: 12 });\n    });\n\n    it(\"resolves expressions inside arrays\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { a: 1, b: 2 },\n      };\n      const result = resolvePropValue(\n        [{ $state: \"/a\" }, { $state: \"/b\" }, 3],\n        ctx,\n      );\n      expect(result).toEqual([1, 2, 3]);\n    });\n\n    it(\"resolves deeply nested expressions\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { theme: { primary: \"#007AFF\" } },\n      };\n      const result = resolvePropValue(\n        { style: { color: { $state: \"/theme/primary\" }, margin: 10 } },\n        ctx,\n      );\n      expect(result).toEqual({ style: { color: \"#007AFF\", margin: 10 } });\n    });\n  });\n});\n\n// =============================================================================\n// resolveElementProps\n// =============================================================================\n\ndescribe(\"resolveElementProps\", () => {\n  it(\"resolves all props in an element\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { user: { name: \"Alice\", role: \"admin\" } },\n    };\n    const props = {\n      label: { $state: \"/user/name\" },\n      badge: { $state: \"/user/role\" },\n      static: \"always\",\n    };\n    expect(resolveElementProps(props, ctx)).toEqual({\n      label: \"Alice\",\n      badge: \"admin\",\n      static: \"always\",\n    });\n  });\n\n  it(\"resolves mixed expressions and literals\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { active: true },\n      repeatItem: { title: \"Item 1\" },\n      repeatIndex: 2,\n    };\n    const props = {\n      title: { $item: \"title\" },\n      index: { $index: true },\n      color: {\n        $cond: { $state: \"/active\" },\n        $then: \"green\",\n        $else: \"gray\",\n      },\n      width: 100,\n    };\n    expect(resolveElementProps(props, ctx)).toEqual({\n      title: \"Item 1\",\n      index: 2,\n      color: \"green\",\n      width: 100,\n    });\n  });\n\n  it(\"returns empty object for empty props\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolveElementProps({}, ctx)).toEqual({});\n  });\n});\n\n// =============================================================================\n// $bindState / $bindItem expressions\n// =============================================================================\n\ndescribe(\"$bindState expressions\", () => {\n  describe(\"resolvePropValue with $bindState\", () => {\n    it(\"resolves to the state value at the path\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { form: { email: \"alice@example.com\" } },\n      };\n      expect(resolvePropValue({ $bindState: \"/form/email\" }, ctx)).toBe(\n        \"alice@example.com\",\n      );\n    });\n\n    it(\"returns undefined for missing path\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      expect(resolvePropValue({ $bindState: \"/missing\" }, ctx)).toBeUndefined();\n    });\n  });\n\n  describe(\"resolvePropValue with $bindItem\", () => {\n    it(\"resolves item field using repeatBasePath\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { todos: [{ completed: true }, { completed: false }] },\n        repeatItem: { completed: true },\n        repeatIndex: 0,\n        repeatBasePath: \"/todos/0\",\n      };\n      expect(resolvePropValue({ $bindItem: \"completed\" }, ctx)).toBe(true);\n    });\n\n    it('handles \"/\" as the full item path', () => {\n      const ctx: PropResolutionContext = {\n        stateModel: { items: [\"hello\", \"world\"] },\n        repeatItem: \"hello\",\n        repeatIndex: 0,\n        repeatBasePath: \"/items/0\",\n      };\n      expect(resolvePropValue({ $bindItem: \"\" }, ctx)).toBe(\"hello\");\n    });\n\n    it(\"returns undefined when no repeatBasePath\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { completed: true },\n        repeatIndex: 0,\n      };\n      // Without repeatBasePath, the raw item path won't resolve in stateModel\n      expect(resolvePropValue({ $bindItem: \"completed\" }, ctx)).toBeUndefined();\n    });\n  });\n\n  describe(\"resolveBindings\", () => {\n    it(\"extracts $bindState paths from props\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      const props = {\n        value: { $bindState: \"/form/email\" },\n        label: \"Email\",\n        placeholder: \"Enter email\",\n      };\n      expect(resolveBindings(props, ctx)).toEqual({\n        value: \"/form/email\",\n      });\n    });\n\n    it(\"returns undefined when no bind expressions\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      const props = {\n        label: \"Hello\",\n        count: 42,\n      };\n      expect(resolveBindings(props, ctx)).toBeUndefined();\n    });\n\n    it(\"handles multiple $bindState props\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      const props = {\n        value: { $bindState: \"/form/name\" },\n        checked: { $bindState: \"/form/agree\" },\n        label: \"Name\",\n      };\n      expect(resolveBindings(props, ctx)).toEqual({\n        value: \"/form/name\",\n        checked: \"/form/agree\",\n      });\n    });\n\n    it(\"resolves $bindItem paths using repeatBasePath\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { completed: false },\n        repeatIndex: 1,\n        repeatBasePath: \"/todos/1\",\n      };\n      const props = {\n        checked: { $bindItem: \"completed\" },\n        label: { $item: \"title\" },\n      };\n      expect(resolveBindings(props, ctx)).toEqual({\n        checked: \"/todos/1/completed\",\n      });\n    });\n\n    it(\"ignores non-bind dynamic expressions\", () => {\n      const ctx: PropResolutionContext = { stateModel: {} };\n      const props = {\n        title: { $state: \"/title\" },\n        index: { $index: true },\n        name: { $item: \"name\" },\n        value: { $bindState: \"/path\" },\n      };\n      expect(resolveBindings(props, ctx)).toEqual({\n        value: \"/path\",\n      });\n    });\n\n    it(\"handles mixed $bindState and $bindItem props\", () => {\n      const ctx: PropResolutionContext = {\n        stateModel: {},\n        repeatItem: { done: false },\n        repeatIndex: 0,\n        repeatBasePath: \"/todos/0\",\n      };\n      const props = {\n        value: { $bindState: \"/form/search\" },\n        checked: { $bindItem: \"done\" },\n        label: \"Task\",\n      };\n      expect(resolveBindings(props, ctx)).toEqual({\n        value: \"/form/search\",\n        checked: \"/todos/0/done\",\n      });\n    });\n  });\n});\n\n// =============================================================================\n// resolveActionParam\n// =============================================================================\n\ndescribe(\"resolveActionParam\", () => {\n  it(\"resolves $item to an absolute state path via repeatBasePath\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { todos: [{ title: \"Buy milk\" }] },\n      repeatItem: { title: \"Buy milk\" },\n      repeatIndex: 0,\n      repeatBasePath: \"/todos/0\",\n    };\n    expect(resolveActionParam({ $item: \"title\" }, ctx)).toBe(\"/todos/0/title\");\n  });\n\n  it(\"resolves $item with empty string to the repeatBasePath itself\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { items: [\"a\", \"b\"] },\n      repeatItem: \"a\",\n      repeatIndex: 0,\n      repeatBasePath: \"/items/0\",\n    };\n    expect(resolveActionParam({ $item: \"\" }, ctx)).toBe(\"/items/0\");\n  });\n\n  it(\"returns undefined for $item when no repeatBasePath\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: {},\n      repeatItem: { title: \"Hello\" },\n      repeatIndex: 0,\n    };\n    expect(resolveActionParam({ $item: \"title\" }, ctx)).toBeUndefined();\n  });\n\n  it(\"resolves $index to the current repeat index\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: {},\n      repeatItem: { id: \"1\" },\n      repeatIndex: 5,\n    };\n    expect(resolveActionParam({ $index: true }, ctx)).toBe(5);\n  });\n\n  it(\"returns undefined for $index when no repeat context\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolveActionParam({ $index: true }, ctx)).toBeUndefined();\n  });\n\n  it(\"delegates $state expressions to resolvePropValue\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { form: { id: \"abc-123\" } },\n    };\n    expect(resolveActionParam({ $state: \"/form/id\" }, ctx)).toBe(\"abc-123\");\n  });\n\n  it(\"passes through literal strings\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolveActionParam(\"submit\", ctx)).toBe(\"submit\");\n  });\n\n  it(\"passes through literal numbers\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolveActionParam(42, ctx)).toBe(42);\n  });\n\n  it(\"passes through null\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolveActionParam(null, ctx)).toBeNull();\n  });\n});\n\n// =============================================================================\n// $computed expressions\n// =============================================================================\n\ndescribe(\"$computed expressions\", () => {\n  it(\"calls a registered function with resolved args\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { form: { firstName: \"Jane\", lastName: \"Doe\" } },\n      functions: {\n        fullName: (args) => `${args.first} ${args.last}`,\n      },\n    };\n    expect(\n      resolvePropValue(\n        {\n          $computed: \"fullName\",\n          args: {\n            first: { $state: \"/form/firstName\" },\n            last: { $state: \"/form/lastName\" },\n          },\n        },\n        ctx,\n      ),\n    ).toBe(\"Jane Doe\");\n  });\n\n  it(\"calls function with no args\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: {},\n      functions: {\n        timestamp: () => 1234567890,\n      },\n    };\n    expect(resolvePropValue({ $computed: \"timestamp\" }, ctx)).toBe(1234567890);\n  });\n\n  it(\"returns undefined for unknown function\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: {},\n      functions: {},\n    };\n    expect(resolvePropValue({ $computed: \"unknown\" }, ctx)).toBeUndefined();\n  });\n\n  it(\"returns undefined when no functions in context\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolvePropValue({ $computed: \"any\" }, ctx)).toBeUndefined();\n  });\n\n  it(\"deduplicates warnings for the same unknown function\", () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const ctx: PropResolutionContext = { stateModel: {}, functions: {} };\n    resolvePropValue({ $computed: \"dedupTest\" }, ctx);\n    resolvePropValue({ $computed: \"dedupTest\" }, ctx);\n    const calls = warnSpy.mock.calls.filter((c) =>\n      String(c[0]).includes(\"dedupTest\"),\n    );\n    expect(calls).toHaveLength(1);\n    warnSpy.mockRestore();\n  });\n\n  it(\"resolves nested expressions in args\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { active: true, values: { a: 10, b: 20 } },\n      functions: {\n        conditionalSum: (args) => {\n          if (args.enabled) return (args.x as number) + (args.y as number);\n          return 0;\n        },\n      },\n    };\n    expect(\n      resolvePropValue(\n        {\n          $computed: \"conditionalSum\",\n          args: {\n            enabled: { $state: \"/active\" },\n            x: { $state: \"/values/a\" },\n            y: { $state: \"/values/b\" },\n          },\n        },\n        ctx,\n      ),\n    ).toBe(30);\n  });\n});\n\n// =============================================================================\n// $template expressions\n// =============================================================================\n\ndescribe(\"$template expressions\", () => {\n  it(\"interpolates state values into a string\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { user: { name: \"Alice\" }, count: 3 },\n    };\n    expect(\n      resolvePropValue(\n        { $template: \"Hello, ${/user/name}! You have ${/count} messages.\" },\n        ctx,\n      ),\n    ).toBe(\"Hello, Alice! You have 3 messages.\");\n  });\n\n  it(\"replaces missing paths with empty string\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolvePropValue({ $template: \"Hi ${/name}!\" }, ctx)).toBe(\"Hi !\");\n  });\n\n  it(\"handles template with no interpolations\", () => {\n    const ctx: PropResolutionContext = { stateModel: {} };\n    expect(resolvePropValue({ $template: \"No variables here\" }, ctx)).toBe(\n      \"No variables here\",\n    );\n  });\n\n  it(\"handles multiple references to the same path\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { x: \"A\" },\n    };\n    expect(resolvePropValue({ $template: \"${/x} and ${/x}\" }, ctx)).toBe(\n      \"A and A\",\n    );\n  });\n\n  it(\"converts non-string values to strings\", () => {\n    const ctx: PropResolutionContext = {\n      stateModel: { num: 42, bool: true },\n    };\n    expect(resolvePropValue({ $template: \"${/num} is ${/bool}\" }, ctx)).toBe(\n      \"42 is true\",\n    );\n  });\n\n  it(\"warns when path does not start with /\", () => {\n    _resetWarnedTemplatePaths();\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const ctx: PropResolutionContext = { stateModel: { name: \"Bob\" } };\n    const result = resolvePropValue({ $template: \"Hi ${name}!\" }, ctx);\n    expect(result).toBe(\"Hi Bob!\");\n    expect(warnSpy).toHaveBeenCalledWith(\n      expect.stringContaining('$template path \"name\"'),\n    );\n    warnSpy.mockRestore();\n    _resetWarnedTemplatePaths();\n  });\n\n  it(\"deduplicates warnings for the same $template path\", () => {\n    _resetWarnedTemplatePaths();\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const ctx: PropResolutionContext = { stateModel: { name: \"Bob\" } };\n    resolvePropValue({ $template: \"Hi ${name}!\" }, ctx);\n    resolvePropValue({ $template: \"Hi ${name}!\" }, ctx);\n    const calls = warnSpy.mock.calls.filter((c) =>\n      String(c[0]).includes('$template path \"name\"'),\n    );\n    expect(calls).toHaveLength(1);\n    warnSpy.mockRestore();\n    _resetWarnedTemplatePaths();\n  });\n});\n"
  },
  {
    "path": "packages/core/src/props.ts",
    "content": "import type { VisibilityCondition, StateModel } from \"./types\";\nimport { getByPath } from \"./types\";\nimport { evaluateVisibility, type VisibilityContext } from \"./visibility\";\n\n// =============================================================================\n// Prop Expression Types\n// =============================================================================\n\n/**\n * A prop expression that resolves to a value based on state.\n *\n * - `{ $state: string }` reads a value from the global state model\n * - `{ $item: string }` reads a field from the current repeat item\n *    (relative path into the item object; use `\"\"` for the whole item)\n * - `{ $index: true }` returns the current repeat array index. Uses `true`\n *    as a sentinel flag because the index is a scalar with no sub-path to\n *    navigate — unlike `$item` which needs a path into the item object.\n * - `{ $bindState: string }` two-way binding to a global state path —\n *    resolves to the value at the path (like `$state`) AND exposes the\n *    resolved path so the component can write back.\n * - `{ $bindItem: string }` two-way binding to a field on the current\n *    repeat item — resolves via `repeatBasePath + path` and exposes the\n *    absolute state path for write-back.\n * - `{ $cond, $then, $else }` conditionally picks a value\n * - `{ $computed: string, args?: Record<string, PropExpression> }` calls a\n *    registered function with resolved args and returns the result\n * - `{ $template: string }` interpolates `${/path}` references in the\n *    string with values from the state model\n * - Any other value is a literal (passthrough)\n */\nexport type PropExpression<T = unknown> =\n  | T\n  | { $state: string }\n  | { $item: string }\n  | { $index: true }\n  | { $bindState: string }\n  | { $bindItem: string }\n  | {\n      $cond: VisibilityCondition;\n      $then: PropExpression<T>;\n      $else: PropExpression<T>;\n    }\n  | { $computed: string; args?: Record<string, unknown> }\n  | { $template: string };\n\n/**\n * Function signature for `$computed` expressions.\n * Receives a record of resolved argument values and returns a computed result.\n */\nexport type ComputedFunction = (args: Record<string, unknown>) => unknown;\n\n/**\n * Context for resolving prop expressions.\n * Extends {@link VisibilityContext} with an optional `repeatBasePath` used\n * to resolve `$bindItem` paths to absolute state paths.\n */\nexport interface PropResolutionContext extends VisibilityContext {\n  /** Absolute state path to the current repeat item (e.g. \"/todos/0\"). Set inside repeat scopes. */\n  repeatBasePath?: string;\n  /** Named functions available for `$computed` expressions. */\n  functions?: Record<string, ComputedFunction>;\n}\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\nfunction isStateExpression(value: unknown): value is { $state: string } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$state\" in value &&\n    typeof (value as Record<string, unknown>).$state === \"string\"\n  );\n}\n\nfunction isItemExpression(value: unknown): value is { $item: string } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$item\" in value &&\n    typeof (value as Record<string, unknown>).$item === \"string\"\n  );\n}\n\nfunction isIndexExpression(value: unknown): value is { $index: true } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$index\" in value &&\n    (value as Record<string, unknown>).$index === true\n  );\n}\n\nfunction isBindStateExpression(\n  value: unknown,\n): value is { $bindState: string } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$bindState\" in value &&\n    typeof (value as Record<string, unknown>).$bindState === \"string\"\n  );\n}\n\nfunction isBindItemExpression(value: unknown): value is { $bindItem: string } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$bindItem\" in value &&\n    typeof (value as Record<string, unknown>).$bindItem === \"string\"\n  );\n}\n\nfunction isCondExpression(\n  value: unknown,\n): value is { $cond: VisibilityCondition; $then: unknown; $else: unknown } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$cond\" in value &&\n    \"$then\" in value &&\n    \"$else\" in value\n  );\n}\n\nfunction isComputedExpression(\n  value: unknown,\n): value is { $computed: string; args?: Record<string, unknown> } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$computed\" in value &&\n    typeof (value as Record<string, unknown>).$computed === \"string\"\n  );\n}\n\nfunction isTemplateExpression(value: unknown): value is { $template: string } {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"$template\" in value &&\n    typeof (value as Record<string, unknown>).$template === \"string\"\n  );\n}\n\n// Module-level set to avoid spamming console.warn on every render for the same\n// unknown $computed function name. Once the set reaches WARNED_COMPUTED_MAX,\n// new names are no longer deduplicated (warnings still fire) but the set stops\n// growing, preventing unbounded memory use in long-lived processes (e.g. SSR).\nconst WARNED_COMPUTED_MAX = 100;\nconst warnedComputedFns = new Set<string>();\n\n/** @internal Test-only: clear the deduplication set for $computed warnings. */\nexport function _resetWarnedComputedFns(): void {\n  warnedComputedFns.clear();\n}\n\n// Same deduplication pattern for $template paths that don't start with \"/\".\nconst WARNED_TEMPLATE_MAX = 100;\nconst warnedTemplatePaths = new Set<string>();\n\n/** @internal Test-only: clear the deduplication set for $template warnings. */\nexport function _resetWarnedTemplatePaths(): void {\n  warnedTemplatePaths.clear();\n}\n\n// =============================================================================\n// Prop Expression Resolution\n// =============================================================================\n\n// =============================================================================\n// $bindItem path resolution helper\n// =============================================================================\n\n/**\n * Resolve a `$bindItem` path into an absolute state path using the repeat\n * scope's base path.\n *\n * `\"\"` resolves to `repeatBasePath` (the whole item).\n * `\"field\"` resolves to `repeatBasePath + \"/field\"`.\n *\n * Returns `undefined` when no `repeatBasePath` is available (i.e. `$bindItem`\n * is used outside a repeat scope).\n */\nfunction resolveBindItemPath(\n  itemPath: string,\n  ctx: PropResolutionContext,\n): string | undefined {\n  if (ctx.repeatBasePath == null) {\n    console.warn(`$bindItem used outside repeat scope: \"${itemPath}\"`);\n    return undefined;\n  }\n  if (itemPath === \"\") return ctx.repeatBasePath;\n  return ctx.repeatBasePath + \"/\" + itemPath;\n}\n\n// =============================================================================\n// Prop Expression Resolution\n// =============================================================================\n\n/**\n * Resolve a single prop value that may contain expressions.\n * Handles $state, $item, $index, $bindState, $bindItem, and $cond/$then/$else in a single pass.\n */\nexport function resolvePropValue(\n  value: unknown,\n  ctx: PropResolutionContext,\n): unknown {\n  if (value === null || value === undefined) {\n    return value;\n  }\n\n  // $state: read from global state model\n  if (isStateExpression(value)) {\n    return getByPath(ctx.stateModel, value.$state);\n  }\n\n  // $item: read from current repeat item\n  if (isItemExpression(value)) {\n    if (ctx.repeatItem === undefined) return undefined;\n    // \"\" means the whole item, \"field\" means a field on the item\n    return value.$item === \"\"\n      ? ctx.repeatItem\n      : getByPath(ctx.repeatItem, value.$item);\n  }\n\n  // $index: return current repeat array index\n  if (isIndexExpression(value)) {\n    return ctx.repeatIndex;\n  }\n\n  // $bindState: two-way binding to global state path\n  if (isBindStateExpression(value)) {\n    return getByPath(ctx.stateModel, value.$bindState);\n  }\n\n  // $bindItem: two-way binding to repeat item field\n  if (isBindItemExpression(value)) {\n    const resolvedPath = resolveBindItemPath(value.$bindItem, ctx);\n    if (resolvedPath === undefined) return undefined;\n    return getByPath(ctx.stateModel, resolvedPath);\n  }\n\n  // $cond/$then/$else: evaluate condition and pick branch\n  if (isCondExpression(value)) {\n    const result = evaluateVisibility(value.$cond, ctx);\n    return resolvePropValue(result ? value.$then : value.$else, ctx);\n  }\n\n  // $computed: call a registered function with resolved args\n  if (isComputedExpression(value)) {\n    const fn = ctx.functions?.[value.$computed];\n    if (!fn) {\n      if (!warnedComputedFns.has(value.$computed)) {\n        if (warnedComputedFns.size < WARNED_COMPUTED_MAX) {\n          warnedComputedFns.add(value.$computed);\n        }\n        console.warn(`Unknown $computed function: \"${value.$computed}\"`);\n      }\n      return undefined;\n    }\n    const resolvedArgs: Record<string, unknown> = {};\n    if (value.args) {\n      for (const [key, arg] of Object.entries(value.args)) {\n        resolvedArgs[key] = resolvePropValue(arg, ctx);\n      }\n    }\n    return fn(resolvedArgs);\n  }\n\n  // $template: interpolate ${/path} references with state values\n  if (isTemplateExpression(value)) {\n    return value.$template.replace(\n      /\\$\\{([^}]+)\\}/g,\n      (_match, rawPath: string) => {\n        let path = rawPath;\n        if (!path.startsWith(\"/\")) {\n          if (!warnedTemplatePaths.has(path)) {\n            if (warnedTemplatePaths.size < WARNED_TEMPLATE_MAX) {\n              warnedTemplatePaths.add(path);\n            }\n            console.warn(\n              `$template path \"${path}\" should be a JSON Pointer starting with \"/\". Automatically resolving as \"/${path}\".`,\n            );\n          }\n          path = \"/\" + path;\n        }\n        const resolved = getByPath(ctx.stateModel, path);\n        return resolved != null ? String(resolved) : \"\";\n      },\n    );\n  }\n\n  // Arrays: resolve each element\n  if (Array.isArray(value)) {\n    return value.map((item) => resolvePropValue(item, ctx));\n  }\n\n  // Plain objects (not expressions): resolve each value recursively\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = resolvePropValue(val, ctx);\n    }\n    return resolved;\n  }\n\n  // Primitive literal: passthrough\n  return value;\n}\n\n/**\n * Resolve all prop values in an element's props object.\n * Returns a new props object with all expressions resolved.\n */\nexport function resolveElementProps(\n  props: Record<string, unknown>,\n  ctx: PropResolutionContext,\n): Record<string, unknown> {\n  const resolved: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(props)) {\n    resolved[key] = resolvePropValue(value, ctx);\n  }\n  return resolved;\n}\n\n/**\n * Scan an element's raw props for `$bindState` / `$bindItem` expressions\n * and return a map of prop name → resolved absolute state path.\n *\n * This is called **before** `resolveElementProps` so the component can\n * receive both the resolved value (in `props`) and the write-back path\n * (in `bindings`).\n *\n * @example\n * ```ts\n * const rawProps = { value: { $bindState: \"/form/email\" }, label: \"Email\" };\n * const bindings = resolveBindings(rawProps, ctx);\n * // bindings = { value: \"/form/email\" }\n * ```\n */\nexport function resolveBindings(\n  props: Record<string, unknown>,\n  ctx: PropResolutionContext,\n): Record<string, string> | undefined {\n  let bindings: Record<string, string> | undefined;\n  for (const [key, value] of Object.entries(props)) {\n    if (isBindStateExpression(value)) {\n      if (!bindings) bindings = {};\n      bindings[key] = value.$bindState;\n    } else if (isBindItemExpression(value)) {\n      const resolved = resolveBindItemPath(value.$bindItem, ctx);\n      if (resolved !== undefined) {\n        if (!bindings) bindings = {};\n        bindings[key] = resolved;\n      }\n    }\n  }\n  return bindings;\n}\n\n/**\n * Resolve a single action parameter value.\n *\n * Like {@link resolvePropValue} but with special handling for path-valued\n * params: `{ $item: \"field\" }` resolves to an **absolute state path**\n * (e.g. `/todos/0/field`) instead of the field's value, so the path can\n * be passed to `setState` / `pushState` / `removeState`.\n *\n * - `{ $item: \"field\" }` → absolute state path via `repeatBasePath`\n * - `{ $index: true }` → current repeat index (number)\n * - Everything else delegates to `resolvePropValue` ($state, $cond, literals).\n */\nexport function resolveActionParam(\n  value: unknown,\n  ctx: PropResolutionContext,\n): unknown {\n  if (isItemExpression(value)) {\n    return resolveBindItemPath(value.$item, ctx);\n  }\n  if (isIndexExpression(value)) {\n    return ctx.repeatIndex;\n  }\n  return resolvePropValue(value, ctx);\n}\n"
  },
  {
    "path": "packages/core/src/schema.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { z } from \"zod\";\nimport { defineSchema, defineCatalog } from \"./schema\";\n\n// =============================================================================\n// Shared test schema (mirrors the React schema shape)\n// =============================================================================\n\nconst testSchema = defineSchema((s) => ({\n  spec: s.object({\n    root: s.string(),\n    elements: s.record(\n      s.object({\n        type: s.ref(\"catalog.components\"),\n        props: s.propsOf(\"catalog.components\"),\n        children: s.array(s.string()),\n        visible: s.any(),\n      }),\n    ),\n  }),\n  catalog: s.object({\n    components: s.map({\n      props: s.zod(),\n      slots: s.array(s.string()),\n      description: s.string(),\n      example: s.any(),\n    }),\n    actions: s.map({\n      description: s.string(),\n    }),\n  }),\n}));\n\n// =============================================================================\n// defineSchema\n// =============================================================================\n\ndescribe(\"defineSchema\", () => {\n  it(\"creates a schema with spec and catalog definition\", () => {\n    const schema = defineSchema((s) => ({\n      spec: s.object({ root: s.string() }),\n      catalog: s.object({ components: s.map({ props: s.zod() }) }),\n    }));\n    expect(schema.definition).toBeDefined();\n    expect(schema.definition.spec.kind).toBe(\"object\");\n    expect(schema.definition.catalog.kind).toBe(\"object\");\n  });\n\n  it(\"accepts promptTemplate option\", () => {\n    const template = () => \"custom prompt\";\n    const schema = defineSchema(\n      (s) => ({\n        spec: s.object({ root: s.string() }),\n        catalog: s.object({ components: s.map({ props: s.zod() }) }),\n      }),\n      { promptTemplate: template },\n    );\n    expect(schema.promptTemplate).toBe(template);\n  });\n\n  it(\"accepts defaultRules option\", () => {\n    const schema = defineSchema(\n      (s) => ({\n        spec: s.object({ root: s.string() }),\n        catalog: s.object({ components: s.map({ props: s.zod() }) }),\n      }),\n      { defaultRules: [\"Rule A\", \"Rule B\"] },\n    );\n    expect(schema.defaultRules).toEqual([\"Rule A\", \"Rule B\"]);\n  });\n\n  it(\"exposes createCatalog method\", () => {\n    const schema = defineSchema((s) => ({\n      spec: s.object({ root: s.string() }),\n      catalog: s.object({ components: s.map({ props: s.zod() }) }),\n    }));\n    expect(typeof schema.createCatalog).toBe(\"function\");\n  });\n});\n\n// =============================================================================\n// defineCatalog / createCatalog\n// =============================================================================\n\ndescribe(\"defineCatalog\", () => {\n  it(\"creates catalog with componentNames and actionNames\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"Display text\",\n          slots: [],\n        },\n        Button: {\n          props: z.object({ label: z.string() }),\n          description: \"A clickable button\",\n          slots: [],\n        },\n      },\n      actions: {\n        navigate: { description: \"Navigate to URL\" },\n        submit: { description: \"Submit form\" },\n      },\n    });\n\n    expect(catalog.componentNames).toEqual([\"Text\", \"Button\"]);\n    expect(catalog.actionNames).toEqual([\"navigate\", \"submit\"]);\n  });\n\n  it(\"handles empty components and actions\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {},\n      actions: {},\n    });\n    expect(catalog.componentNames).toEqual([]);\n    expect(catalog.actionNames).toEqual([]);\n  });\n\n  it(\"is equivalent to schema.createCatalog\", () => {\n    const catalogData = {\n      components: {\n        Card: {\n          props: z.object({ title: z.string() }),\n          description: \"A card\",\n          slots: [\"default\"],\n        },\n      },\n      actions: {},\n    };\n\n    const a = defineCatalog(testSchema, catalogData);\n    const b = testSchema.createCatalog(catalogData);\n\n    expect(a.componentNames).toEqual(b.componentNames);\n    expect(a.actionNames).toEqual(b.actionNames);\n    expect(a.data).toBe(b.data);\n  });\n\n  it(\"exposes the schema on the catalog\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {},\n      actions: {},\n    });\n    expect(catalog.schema).toBe(testSchema);\n  });\n\n  it(\"exposes catalog data\", () => {\n    const data = {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    };\n    const catalog = defineCatalog(testSchema, data);\n    expect(catalog.data).toBe(data);\n  });\n});\n\n// =============================================================================\n// catalog.prompt()\n// =============================================================================\n\ndescribe(\"catalog.prompt\", () => {\n  it(\"includes AVAILABLE COMPONENTS section\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Card: {\n          props: z.object({\n            title: z.string(),\n            names: z.array(z.string()),\n            users: z.array(z.object({ name: z.string(), age: z.number() })),\n          }),\n          description: \"A card container\",\n          slots: [\"default\"],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain(\"AVAILABLE COMPONENTS\");\n    expect(prompt).toContain(\"Card\");\n    expect(prompt).toContain(\"A card container\");\n    expect(prompt).toContain(\"title: string\");\n    expect(prompt).toContain(\"names: Array<string>\");\n    expect(prompt).toContain(\"users: Array<{ name: string, age: number }>\");\n  });\n\n  it(\"includes AVAILABLE ACTIONS when present\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {\n        navigate: { description: \"Navigate to URL\" },\n      },\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain(\"AVAILABLE ACTIONS\");\n    expect(prompt).toContain(\"navigate\");\n    expect(prompt).toContain(\"Navigate to URL\");\n  });\n\n  it(\"omits AVAILABLE ACTIONS when there are none\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).not.toContain(\"AVAILABLE ACTIONS\");\n  });\n\n  it(\"uses custom system message when provided\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt({ system: \"You are a dashboard builder.\" });\n    expect(prompt).toContain(\"You are a dashboard builder.\");\n    expect(prompt).not.toContain(\"You are a UI generator that outputs JSON.\");\n  });\n\n  it(\"appends customRules to prompt\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt({\n      customRules: [\"Always use Card as root\", \"Keep UIs simple\"],\n    });\n    expect(prompt).toContain(\"Always use Card as root\");\n    expect(prompt).toContain(\"Keep UIs simple\");\n  });\n\n  it(\"generates inline mode prompt when mode is inline\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt({ mode: \"inline\" });\n    expect(prompt).toContain(\"```spec\");\n    expect(prompt).toContain(\"conversationally\");\n    expect(prompt).toContain(\"text + JSONL\");\n  });\n\n  it(\"generates standalone mode prompt by default\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain(\"Output ONLY JSONL patches\");\n    expect(prompt).not.toContain(\"conversationally\");\n  });\n\n  it(\"accepts deprecated 'chat' as alias for 'inline'\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const inlinePrompt = catalog.prompt({ mode: \"inline\" });\n    const chatPrompt = catalog.prompt({ mode: \"chat\" });\n    expect(chatPrompt).toEqual(inlinePrompt);\n  });\n\n  it(\"accepts deprecated 'generate' as alias for 'standalone'\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const standalonePrompt = catalog.prompt({ mode: \"standalone\" });\n    const generatePrompt = catalog.prompt({ mode: \"generate\" });\n    expect(generatePrompt).toEqual(standalonePrompt);\n  });\n\n  it(\"uses actual catalog component names in examples\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        MyBox: {\n          props: z.object({ padding: z.number() }),\n          description: \"A box\",\n          slots: [\"default\"],\n        },\n        MyLabel: {\n          props: z.object({ text: z.string() }),\n          description: \"A label\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain('\"type\":\"MyBox\"');\n    expect(prompt).toContain('\"type\":\"MyLabel\"');\n  });\n\n  it(\"does not include hardcoded component names not in catalog\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    const hardcoded = [\"Stack\", \"Grid\", \"Heading\", \"Column\", \"Pressable\"];\n    for (const comp of hardcoded) {\n      expect(prompt).not.toContain(`\"type\":\"${comp}\"`);\n    }\n  });\n\n  it(\"generates example props from Zod schemas\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Widget: {\n          props: z.object({\n            title: z.string(),\n            count: z.number(),\n            active: z.boolean(),\n            variant: z.enum([\"primary\", \"secondary\"]),\n          }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain('\"title\":\"example\"');\n    expect(prompt).toContain('\"count\":0');\n    expect(prompt).toContain('\"active\":true');\n    expect(prompt).toContain('\"variant\":\"primary\"');\n  });\n\n  it(\"uses explicit example over Zod-generated values\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Heading: {\n          props: z.object({\n            text: z.string(),\n            level: z.enum([\"h1\", \"h2\", \"h3\"]),\n          }),\n          description: \"A heading\",\n          slots: [],\n          example: { text: \"Welcome\", level: \"h1\" },\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain('\"text\":\"Welcome\"');\n    expect(prompt).toContain('\"level\":\"h1\"');\n  });\n\n  it(\"uses custom promptTemplate when schema has one\", () => {\n    const customSchema = defineSchema(\n      (s) => ({\n        spec: s.object({ root: s.string() }),\n        catalog: s.object({\n          components: s.map({ props: s.zod(), description: s.string() }),\n        }),\n      }),\n      {\n        promptTemplate: (ctx) =>\n          `Custom prompt with ${ctx.componentNames.length} components: ${ctx.componentNames.join(\", \")}`,\n      },\n    );\n    const catalog = customSchema.createCatalog({\n      components: {\n        Alpha: { props: z.object({}), description: \"A\" },\n        Beta: { props: z.object({}), description: \"B\" },\n      },\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toBe(\"Custom prompt with 2 components: Alpha, Beta\");\n  });\n\n  it(\"includes defaultRules from schema in the RULES section\", () => {\n    const schemaWithRules = defineSchema(\n      (s) => ({\n        spec: s.object({\n          root: s.string(),\n          elements: s.record(\n            s.object({\n              type: s.ref(\"catalog.components\"),\n              props: s.any(),\n              children: s.array(s.string()),\n            }),\n          ),\n        }),\n        catalog: s.object({\n          components: s.map({ props: s.zod(), description: s.string() }),\n        }),\n      }),\n      { defaultRules: [\"Schema default rule one\", \"Schema default rule two\"] },\n    );\n    const catalog = schemaWithRules.createCatalog({\n      components: {\n        Text: { props: z.object({}), description: \"\" },\n      },\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain(\"Schema default rule one\");\n    expect(prompt).toContain(\"Schema default rule two\");\n  });\n\n  it(\"contains sections for state, repeat, actions, visibility, and dynamic props\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const prompt = catalog.prompt();\n    expect(prompt).toContain(\"INITIAL STATE:\");\n    expect(prompt).toContain(\"DYNAMIC LISTS (repeat field):\");\n    expect(prompt).toContain(\"EVENTS (the `on` field):\");\n    expect(prompt).toContain(\"VISIBILITY CONDITIONS:\");\n    expect(prompt).toContain(\"DYNAMIC PROPS:\");\n    expect(prompt).toContain(\"RULES:\");\n  });\n});\n\n// =============================================================================\n// catalog.validate()\n// =============================================================================\n\ndescribe(\"catalog.validate\", () => {\n  const catalog = defineCatalog(testSchema, {\n    components: {\n      Text: {\n        props: z.object({ content: z.string() }),\n        description: \"\",\n        slots: [],\n      },\n      Card: {\n        props: z.object({ title: z.string() }),\n        description: \"\",\n        slots: [\"default\"],\n      },\n    },\n    actions: {},\n  });\n\n  it(\"validates a valid spec\", () => {\n    const spec = {\n      root: \"card-1\",\n      elements: {\n        \"card-1\": {\n          type: \"Card\",\n          props: { title: \"Hello\" },\n          children: [\"text-1\"],\n        },\n        \"text-1\": {\n          type: \"Text\",\n          props: { content: \"World\" },\n          children: [],\n        },\n      },\n    };\n    const result = catalog.validate(spec);\n    expect(result.success).toBe(true);\n    expect(result.data).toEqual(spec);\n  });\n\n  it(\"rejects spec with wrong root type\", () => {\n    const result = catalog.validate({ root: 123, elements: {} });\n    expect(result.success).toBe(false);\n    expect(result.error).toBeDefined();\n  });\n\n  it(\"rejects spec with missing root\", () => {\n    const result = catalog.validate({ elements: {} });\n    expect(result.success).toBe(false);\n    expect(result.error).toBeDefined();\n  });\n\n  it(\"rejects spec with invalid component type\", () => {\n    const result = catalog.validate({\n      root: \"x\",\n      elements: {\n        x: { type: \"NonExistent\", props: {}, children: [] },\n      },\n    });\n    expect(result.success).toBe(false);\n  });\n\n  it(\"returns data on success\", () => {\n    const spec = {\n      root: \"t\",\n      elements: {\n        t: { type: \"Text\", props: { content: \"hi\" }, children: [] },\n      },\n    };\n    const result = catalog.validate(spec);\n    expect(result.success).toBe(true);\n    expect(result.data).toBeDefined();\n    expect(result.data!.root).toBe(\"t\");\n  });\n});\n\n// =============================================================================\n// catalog.jsonSchema()\n// =============================================================================\n\ndescribe(\"catalog.jsonSchema\", () => {\n  it(\"returns a JSON Schema object\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const jsonSchema = catalog.jsonSchema();\n    expect(jsonSchema).toBeDefined();\n    expect(typeof jsonSchema).toBe(\"object\");\n  });\n\n  it(\"returns a non-empty object for a catalog with components\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const jsonSchema = catalog.jsonSchema();\n    expect(jsonSchema).toBeDefined();\n    expect(jsonSchema).not.toBeNull();\n    expect(typeof jsonSchema).toBe(\"object\");\n  });\n\n  describe(\"strict mode (LLM structured output compatible)\", () => {\n    function hasNoPropertyNames(obj: unknown): boolean {\n      if (typeof obj !== \"object\" || obj === null) return true;\n      if (\"propertyNames\" in obj) return false;\n      return Object.values(obj).every(hasNoPropertyNames);\n    }\n\n    function allObjectsHaveAdditionalPropertiesFalse(obj: unknown): boolean {\n      if (typeof obj !== \"object\" || obj === null) return true;\n      const record = obj as Record<string, unknown>;\n      if (record.type === \"object\") {\n        if (record.additionalProperties !== false) return false;\n      }\n      return Object.values(record).every(\n        allObjectsHaveAdditionalPropertiesFalse,\n      );\n    }\n\n    function allObjectPropertiesRequired(obj: unknown): boolean {\n      if (typeof obj !== \"object\" || obj === null) return true;\n      const record = obj as Record<string, unknown>;\n      if (\n        record.type === \"object\" &&\n        record.properties &&\n        typeof record.properties === \"object\"\n      ) {\n        const propKeys = Object.keys(record.properties);\n        const required = (record.required as string[]) ?? [];\n        if (!propKeys.every((k) => required.includes(k))) return false;\n      }\n      return Object.values(record).every(allObjectPropertiesRequired);\n    }\n\n    it(\"sets additionalProperties: false on all nested objects\", () => {\n      const catalog = defineCatalog(testSchema, {\n        components: {\n          Card: {\n            props: z.object({\n              title: z.string(),\n              subtitle: z.string().optional(),\n            }),\n            description: \"\",\n            slots: [],\n          },\n        },\n        actions: {},\n      });\n      const schema = catalog.jsonSchema({ strict: true });\n      expect(allObjectsHaveAdditionalPropertiesFalse(schema)).toBe(true);\n    });\n\n    it(\"does not emit propertyNames\", () => {\n      const catalog = defineCatalog(testSchema, {\n        components: {\n          Text: {\n            props: z.object({ content: z.string() }),\n            description: \"\",\n            slots: [],\n          },\n        },\n        actions: {},\n      });\n      const schema = catalog.jsonSchema({ strict: true });\n      expect(hasNoPropertyNames(schema)).toBe(true);\n    });\n\n    it(\"lists all properties in required (optional uses nullable)\", () => {\n      const catalog = defineCatalog(testSchema, {\n        components: {\n          Card: {\n            props: z.object({\n              title: z.string(),\n              subtitle: z.string().optional(),\n            }),\n            description: \"\",\n            slots: [],\n          },\n        },\n        actions: {},\n      });\n      const schema = catalog.jsonSchema({ strict: true });\n      expect(allObjectPropertiesRequired(schema)).toBe(true);\n    });\n\n    it(\"converts record types without additionalProperties schema value\", () => {\n      const catalog = defineCatalog(testSchema, {\n        components: {\n          Text: {\n            props: z.object({ content: z.string() }),\n            description: \"\",\n            slots: [],\n          },\n        },\n        actions: {},\n      });\n      const schema = catalog.jsonSchema({\n        strict: true,\n      }) as Record<string, unknown>;\n\n      // Walk the schema and ensure no additionalProperties is set to a non-false value\n      function noAdditionalPropertiesSchema(obj: unknown): boolean {\n        if (typeof obj !== \"object\" || obj === null) return true;\n        const rec = obj as Record<string, unknown>;\n        if (\n          \"additionalProperties\" in rec &&\n          rec.additionalProperties !== false\n        ) {\n          return false;\n        }\n        return Object.values(rec).every(noAdditionalPropertiesSchema);\n      }\n      expect(noAdditionalPropertiesSchema(schema)).toBe(true);\n    });\n\n    it(\"wraps optional properties with anyOf nullable\", () => {\n      // Use a schema where propsOf resolves to a single component's props\n      // (no record wrapper around the props) so the optional anyOf handling\n      // is directly visible in the JSON Schema output.\n      const flatSchema = defineSchema((s) => ({\n        spec: s.object({\n          component: s.object({\n            type: s.ref(\"catalog.components\"),\n            props: s.propsOf(\"catalog.components\"),\n          }),\n        }),\n        catalog: s.object({\n          components: s.map({\n            props: s.zod(),\n            description: s.string(),\n          }),\n        }),\n      }));\n\n      const catalog = defineCatalog(flatSchema, {\n        components: {\n          Card: {\n            props: z.object({\n              heading: z.string(),\n              caption: z.string().optional(),\n            }),\n            description: \"\",\n          },\n        },\n      });\n\n      const schema = catalog.jsonSchema({ strict: true }) as {\n        properties: {\n          component: {\n            properties: { props: Record<string, unknown> };\n          };\n        };\n      };\n\n      const propsSchema = schema.properties.component.properties.props;\n\n      // caption is optional – in strict mode it must be in `required`\n      // and wrapped in anyOf with null\n      const captionSchema = (\n        propsSchema as {\n          properties: { caption: Record<string, unknown> };\n        }\n      ).properties.caption;\n      expect(captionSchema).toEqual({\n        anyOf: [{ type: \"string\" }, { type: \"null\" }],\n      });\n\n      const propsRequired = (propsSchema as { required: string[] }).required;\n      expect(propsRequired).toContain(\"heading\");\n      expect(propsRequired).toContain(\"caption\");\n    });\n\n    it(\"does not affect default (non-strict) output\", () => {\n      const catalog = defineCatalog(testSchema, {\n        components: {\n          Text: {\n            props: z.object({ content: z.string() }),\n            description: \"\",\n            slots: [],\n          },\n        },\n        actions: {},\n      });\n      const defaultSchema = catalog.jsonSchema();\n      const defaultSchema2 = catalog.jsonSchema({ strict: false });\n      expect(defaultSchema).toEqual(defaultSchema2);\n    });\n  });\n});\n\n// =============================================================================\n// catalog.zodSchema()\n// =============================================================================\n\ndescribe(\"catalog.zodSchema\", () => {\n  it(\"returns a Zod schema that validates valid specs\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({ content: z.string() }),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const zodSchema = catalog.zodSchema();\n    const result = zodSchema.safeParse({\n      root: \"t\",\n      elements: {\n        t: { type: \"Text\", props: { content: \"hi\" }, children: [] },\n      },\n    });\n    expect(result.success).toBe(true);\n  });\n\n  it(\"returns a Zod schema that rejects invalid specs\", () => {\n    const catalog = defineCatalog(testSchema, {\n      components: {\n        Text: {\n          props: z.object({}),\n          description: \"\",\n          slots: [],\n        },\n      },\n      actions: {},\n    });\n    const zodSchema = catalog.zodSchema();\n    const result = zodSchema.safeParse({ root: 42 });\n    expect(result.success).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/schema.ts",
    "content": "import { z } from \"zod\";\nimport type { EditMode } from \"./edit-modes\";\nimport { buildEditInstructions } from \"./edit-modes\";\n\n/**\n * Schema builder primitives\n */\nexport interface SchemaBuilder {\n  /** String type */\n  string(): SchemaType<\"string\">;\n  /** Number type */\n  number(): SchemaType<\"number\">;\n  /** Boolean type */\n  boolean(): SchemaType<\"boolean\">;\n  /** Array of type */\n  array<T extends SchemaType>(item: T): SchemaType<\"array\", T>;\n  /** Object with shape */\n  object<T extends Record<string, SchemaType>>(\n    shape: T,\n  ): SchemaType<\"object\", T>;\n  /** Record/map with value type */\n  record<T extends SchemaType>(value: T): SchemaType<\"record\", T>;\n  /** Any type */\n  any(): SchemaType<\"any\">;\n  /** Placeholder for user-provided Zod schema */\n  zod(): SchemaType<\"zod\">;\n  /** Reference to catalog key (e.g., 'catalog.components') */\n  ref(path: string): SchemaType<\"ref\", string>;\n  /** Props from referenced catalog entry */\n  propsOf(path: string): SchemaType<\"propsOf\", string>;\n  /** Map of named entries with shared shape */\n  map<T extends Record<string, SchemaType>>(\n    entryShape: T,\n  ): SchemaType<\"map\", T>;\n  /** Optional modifier */\n  optional(): { optional: true };\n}\n\n/**\n * Schema type representation\n */\nexport interface SchemaType<TKind extends string = string, TInner = unknown> {\n  kind: TKind;\n  inner?: TInner;\n  optional?: boolean;\n}\n\n/**\n * Schema definition shape\n */\nexport interface SchemaDefinition<\n  TSpec extends SchemaType = SchemaType,\n  TCatalog extends SchemaType = SchemaType,\n> {\n  /** What the AI-generated spec looks like */\n  spec: TSpec;\n  /** What the catalog must provide */\n  catalog: TCatalog;\n}\n\n/**\n * Schema instance with methods\n */\nexport interface Schema<TDef extends SchemaDefinition = SchemaDefinition> {\n  /** The schema definition */\n  readonly definition: TDef;\n  /** Custom prompt template for this schema */\n  readonly promptTemplate?: PromptTemplate;\n  /** Default rules baked into the schema (injected before customRules) */\n  readonly defaultRules?: string[];\n  /** Built-in actions always available at runtime (injected into prompts automatically) */\n  readonly builtInActions?: BuiltInAction[];\n  /** Create a catalog from this schema */\n  createCatalog<TCatalog extends InferCatalogInput<TDef[\"catalog\"]>>(\n    catalog: TCatalog,\n  ): Catalog<TDef, TCatalog>;\n}\n\n/**\n * Catalog instance with methods\n */\nexport interface Catalog<\n  TDef extends SchemaDefinition = SchemaDefinition,\n  TCatalog = unknown,\n> {\n  /** The schema this catalog is based on */\n  readonly schema: Schema<TDef>;\n  /** The catalog data */\n  readonly data: TCatalog;\n  /** Component names */\n  readonly componentNames: string[];\n  /** Action names */\n  readonly actionNames: string[];\n  /** Generate system prompt for AI */\n  prompt(options?: PromptOptions): string;\n  /** Export as JSON Schema for structured outputs */\n  jsonSchema(options?: JsonSchemaOptions): object;\n  /** Validate a spec against this catalog */\n  validate(spec: unknown): SpecValidationResult<InferSpec<TDef, TCatalog>>;\n  /** Get the Zod schema for the spec */\n  zodSchema(): z.ZodType<InferSpec<TDef, TCatalog>>;\n  /** Type helper for the spec type */\n  readonly _specType: InferSpec<TDef, TCatalog>;\n}\n\n/**\n * Options for JSON Schema export\n */\nexport interface JsonSchemaOptions {\n  /**\n   * When true, produces a strict JSON Schema subset compatible with\n   * LLM structured output APIs (OpenAI, Google Gemini, Anthropic, etc.).\n   * This ensures:\n   * - `additionalProperties: false` on every object\n   * - All object properties listed in `required` (optionals use nullable types)\n   * - Record/map types converted to fixed-key objects\n   *\n   * **Limitation:** Record types (dynamic-key maps) cannot be represented in\n   * strict JSON Schema because `additionalProperties` must be `false`. They\n   * are emitted as `{ type: \"object\", properties: {}, additionalProperties: false }`.\n   * The LLM prompt (via `catalog.prompt()`) still describes the full structure,\n   * so the model can produce valid output even though the JSON Schema for\n   * record entries is opaque.\n   */\n  strict?: boolean;\n}\n\n/**\n * Prompt generation options\n */\nexport interface PromptOptions {\n  /** Custom system message intro */\n  system?: string;\n  /** Additional rules to append */\n  customRules?: string[];\n  /**\n   * Output mode for the generated prompt.\n   *\n   * - `\"standalone\"` (default): The LLM should output only JSONL patches (no prose).\n   * - `\"inline\"`: The LLM should respond conversationally first, then output JSONL patches.\n   *   Includes rules about interleaving text with JSONL and not wrapping in code fences.\n   *\n   * @deprecated `\"generate\"` — use `\"standalone\"` instead.\n   * @deprecated `\"chat\"` — use `\"inline\"` instead.\n   */\n  mode?: \"standalone\" | \"inline\" | \"generate\" | \"chat\";\n  /** Edit modes to document in the system prompt. Default: `[\"patch\"]`. */\n  editModes?: EditMode[];\n}\n\n/**\n * Context provided to prompt templates\n */\nexport interface PromptContext<TCatalog = unknown> {\n  /** The catalog data */\n  catalog: TCatalog;\n  /** Component names from the catalog */\n  componentNames: string[];\n  /** Action names from the catalog (if any) */\n  actionNames: string[];\n  /** Prompt options provided by the user */\n  options: PromptOptions;\n  /** Helper to format a Zod type as a human-readable string */\n  formatZodType: (schema: z.ZodType) => string;\n}\n\n/**\n * Prompt template function type\n */\nexport type PromptTemplate<TCatalog = unknown> = (\n  context: PromptContext<TCatalog>,\n) => string;\n\n/**\n * A built-in action that is always available regardless of catalog configuration.\n * These are handled by the runtime (e.g. ActionProvider) and injected into prompts\n * automatically so the LLM knows about them.\n */\nexport interface BuiltInAction {\n  /** Action name (e.g. \"setState\") */\n  name: string;\n  /** Human-readable description for the LLM */\n  description: string;\n}\n\n/**\n * Schema options\n */\nexport interface SchemaOptions<TCatalog = unknown> {\n  /** Custom prompt template for this schema */\n  promptTemplate?: PromptTemplate<TCatalog>;\n  /** Default rules baked into the schema (injected before customRules in prompts) */\n  defaultRules?: string[];\n  /**\n   * Built-in actions that are always available regardless of catalog configuration.\n   * These are injected into prompts automatically so the LLM knows about them,\n   * but they don't require handlers in defineRegistry because the runtime\n   * (e.g. ActionProvider) handles them directly.\n   */\n  builtInActions?: BuiltInAction[];\n}\n\n/**\n * Spec validation result\n */\nexport interface SpecValidationResult<T> {\n  success: boolean;\n  data?: T;\n  error?: z.ZodError;\n}\n\n// =============================================================================\n// Catalog Type Inference Helpers\n// =============================================================================\n\n/**\n * Extract the components map type from a catalog\n * @example type Components = InferCatalogComponents<typeof myCatalog>;\n */\nexport type InferCatalogComponents<C extends Catalog> =\n  C extends Catalog<SchemaDefinition, infer TCatalog>\n    ? TCatalog extends { components: infer Comps }\n      ? Comps\n      : never\n    : never;\n\n/**\n * Extract the actions map type from a catalog\n * @example type Actions = InferCatalogActions<typeof myCatalog>;\n */\nexport type InferCatalogActions<C extends Catalog> =\n  C extends Catalog<SchemaDefinition, infer TCatalog>\n    ? TCatalog extends { actions: infer Acts }\n      ? Acts\n      : never\n    : never;\n\n/**\n * Infer component props from a catalog by component name\n * @example type ButtonProps = InferComponentProps<typeof myCatalog, 'Button'>;\n */\nexport type InferComponentProps<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = InferCatalogComponents<C>[K] extends { props: z.ZodType<infer P> }\n  ? P\n  : never;\n\n/**\n * Infer action params from a catalog by action name\n * @example type ViewCustomersParams = InferActionParams<typeof myCatalog, 'viewCustomers'>;\n */\nexport type InferActionParams<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = InferCatalogActions<C>[K] extends { params: z.ZodType<infer P> }\n  ? P\n  : never;\n\n// =============================================================================\n// Internal Type Inference Helpers\n// =============================================================================\n\nexport type InferCatalogInput<T> =\n  T extends SchemaType<\"object\", infer Shape>\n    ? { [K in keyof Shape]: InferCatalogField<Shape[K]> }\n    : never;\n\ntype InferCatalogField<T> =\n  T extends SchemaType<\"map\", infer EntryShape>\n    ? Record<\n        string,\n        // Only 'props' is required, rest are optional\n        InferMapEntryRequired<EntryShape> &\n          Partial<InferMapEntryOptional<EntryShape>>\n      >\n    : T extends SchemaType<\"zod\">\n      ? z.ZodType\n      : T extends SchemaType<\"string\">\n        ? string\n        : T extends SchemaType<\"number\">\n          ? number\n          : T extends SchemaType<\"boolean\">\n            ? boolean\n            : T extends SchemaType<\"array\", infer Item>\n              ? InferCatalogField<Item>[]\n              : T extends SchemaType<\"object\", infer Shape>\n                ? { [K in keyof Shape]: InferCatalogField<Shape[K]> }\n                : unknown;\n\n// Extract required fields (props is always required)\ntype InferMapEntryRequired<T> = {\n  [K in keyof T as K extends \"props\" ? K : never]: InferMapEntryField<T[K]>;\n};\n\n// Extract optional fields (everything except props)\ntype InferMapEntryOptional<T> = {\n  [K in keyof T as K extends \"props\" ? never : K]: InferMapEntryField<T[K]>;\n};\n\ntype InferMapEntryField<T> =\n  T extends SchemaType<\"zod\">\n    ? z.ZodType\n    : T extends SchemaType<\"string\">\n      ? string\n      : T extends SchemaType<\"number\">\n        ? number\n        : T extends SchemaType<\"boolean\">\n          ? boolean\n          : T extends SchemaType<\"array\", infer Item>\n            ? InferMapEntryField<Item>[]\n            : T extends SchemaType<\"object\", infer Shape>\n              ? { [K in keyof Shape]: InferMapEntryField<Shape[K]> }\n              : unknown;\n\n// Spec inference (simplified - will be expanded)\nexport type InferSpec<TDef extends SchemaDefinition, TCatalog> = TDef extends {\n  spec: SchemaType<\"object\", infer Shape>;\n}\n  ? InferSpecObject<Shape, TCatalog>\n  : unknown;\n\ntype InferSpecObject<Shape, TCatalog> = {\n  [K in keyof Shape]: InferSpecField<Shape[K], TCatalog>;\n};\n\ntype InferSpecField<T, TCatalog> =\n  T extends SchemaType<\"string\">\n    ? string\n    : T extends SchemaType<\"number\">\n      ? number\n      : T extends SchemaType<\"boolean\">\n        ? boolean\n        : T extends SchemaType<\"array\", infer Item>\n          ? InferSpecField<Item, TCatalog>[]\n          : T extends SchemaType<\"object\", infer Shape>\n            ? InferSpecObject<Shape, TCatalog>\n            : T extends SchemaType<\"record\", infer Value>\n              ? Record<string, InferSpecField<Value, TCatalog>>\n              : T extends SchemaType<\"ref\", infer Path>\n                ? InferRefType<Path, TCatalog>\n                : T extends SchemaType<\"propsOf\", infer Path>\n                  ? InferPropsOfType<Path, TCatalog>\n                  : T extends SchemaType<\"any\">\n                    ? unknown\n                    : unknown;\n\ntype InferRefType<Path, TCatalog> = Path extends \"catalog.components\"\n  ? TCatalog extends { components: infer C }\n    ? keyof C\n    : string\n  : Path extends \"catalog.actions\"\n    ? TCatalog extends { actions: infer A }\n      ? keyof A\n      : string\n    : string;\n\ntype InferPropsOfType<Path, TCatalog> = Path extends \"catalog.components\"\n  ? TCatalog extends { components: infer C }\n    ? C extends Record<string, { props: z.ZodType<infer P> }>\n      ? P\n      : Record<string, unknown>\n    : Record<string, unknown>\n  : Record<string, unknown>;\n\n/**\n * Create the schema builder\n */\nfunction createBuilder(): SchemaBuilder {\n  return {\n    string: () => ({ kind: \"string\" }),\n    number: () => ({ kind: \"number\" }),\n    boolean: () => ({ kind: \"boolean\" }),\n    array: (item) => ({ kind: \"array\", inner: item }),\n    object: (shape) => ({ kind: \"object\", inner: shape }),\n    record: (value) => ({ kind: \"record\", inner: value }),\n    any: () => ({ kind: \"any\" }),\n    zod: () => ({ kind: \"zod\" }),\n    ref: (path) => ({ kind: \"ref\", inner: path }),\n    propsOf: (path) => ({ kind: \"propsOf\", inner: path }),\n    map: (entryShape) => ({ kind: \"map\", inner: entryShape }),\n    optional: () => ({ optional: true }),\n  };\n}\n\n/**\n * Define a schema using the builder pattern\n */\nexport function defineSchema<TDef extends SchemaDefinition>(\n  builder: (s: SchemaBuilder) => TDef,\n  options?: SchemaOptions,\n): Schema<TDef> {\n  const s = createBuilder();\n  const definition = builder(s);\n\n  return {\n    definition,\n    promptTemplate: options?.promptTemplate,\n    defaultRules: options?.defaultRules,\n    builtInActions: options?.builtInActions,\n    createCatalog<TCatalog extends InferCatalogInput<TDef[\"catalog\"]>>(\n      catalog: TCatalog,\n    ): Catalog<TDef, TCatalog> {\n      return createCatalogFromSchema(this as Schema<TDef>, catalog);\n    },\n  };\n}\n\n/**\n * Create a catalog from a schema (internal)\n */\nfunction createCatalogFromSchema<TDef extends SchemaDefinition, TCatalog>(\n  schema: Schema<TDef>,\n  catalogData: TCatalog,\n): Catalog<TDef, TCatalog> {\n  // Extract component and action names\n  const components = (catalogData as Record<string, unknown>).components as\n    | Record<string, unknown>\n    | undefined;\n  const actions = (catalogData as Record<string, unknown>).actions as\n    | Record<string, unknown>\n    | undefined;\n\n  const componentNames = components ? Object.keys(components) : [];\n  const actionNames = actions ? Object.keys(actions) : [];\n\n  // Build the Zod schema for validation\n  const zodSchema = buildZodSchemaFromDefinition(\n    schema.definition,\n    catalogData,\n  );\n\n  return {\n    schema,\n    data: catalogData,\n    componentNames,\n    actionNames,\n\n    prompt(options: PromptOptions = {}): string {\n      return generatePrompt(this, options);\n    },\n\n    jsonSchema(options: JsonSchemaOptions = {}): object {\n      return zodToJsonSchema(zodSchema, options.strict ?? false);\n    },\n\n    validate(spec: unknown): SpecValidationResult<InferSpec<TDef, TCatalog>> {\n      const result = zodSchema.safeParse(spec);\n      if (result.success) {\n        return {\n          success: true,\n          data: result.data as InferSpec<TDef, TCatalog>,\n        };\n      }\n      return { success: false, error: result.error };\n    },\n\n    zodSchema(): z.ZodType<InferSpec<TDef, TCatalog>> {\n      return zodSchema as z.ZodType<InferSpec<TDef, TCatalog>>;\n    },\n\n    get _specType(): InferSpec<TDef, TCatalog> {\n      throw new Error(\"_specType is only for type inference\");\n    },\n  };\n}\n\n/**\n * Build Zod schema from schema definition\n */\nfunction buildZodSchemaFromDefinition(\n  definition: SchemaDefinition,\n  catalogData: unknown,\n): z.ZodType {\n  return buildZodType(definition.spec, catalogData);\n}\n\nfunction buildZodType(schemaType: SchemaType, catalogData: unknown): z.ZodType {\n  switch (schemaType.kind) {\n    case \"string\":\n      return z.string();\n    case \"number\":\n      return z.number();\n    case \"boolean\":\n      return z.boolean();\n    case \"any\":\n      return z.any();\n    case \"array\": {\n      const inner = buildZodType(schemaType.inner as SchemaType, catalogData);\n      return z.array(inner);\n    }\n    case \"object\": {\n      const shape = schemaType.inner as Record<string, SchemaType>;\n      const zodShape: Record<string, z.ZodType> = {};\n      for (const [key, value] of Object.entries(shape)) {\n        let zodType = buildZodType(value, catalogData);\n        if (value.optional) {\n          zodType = zodType.optional();\n        }\n        zodShape[key] = zodType;\n      }\n      return z.object(zodShape);\n    }\n    case \"record\": {\n      const inner = buildZodType(schemaType.inner as SchemaType, catalogData);\n      return z.record(z.string(), inner);\n    }\n    case \"ref\": {\n      // Reference to catalog key - create enum of valid keys\n      const path = schemaType.inner as string;\n      const keys = getKeysFromPath(path, catalogData);\n      if (keys.length === 0) {\n        return z.string();\n      }\n      if (keys.length === 1) {\n        return z.literal(keys[0]!);\n      }\n      return z.enum(keys as [string, ...string[]]);\n    }\n    case \"propsOf\": {\n      // Props from catalog entry - create union of all props schemas\n      const path = schemaType.inner as string;\n      const propsSchemas = getPropsFromPath(path, catalogData);\n      if (propsSchemas.length === 0) {\n        return z.record(z.string(), z.unknown());\n      }\n      if (propsSchemas.length === 1) {\n        return propsSchemas[0]!;\n      }\n      // For propsOf, we need to be lenient since type determines which props apply\n      return z.record(z.string(), z.unknown());\n    }\n    default:\n      return z.unknown();\n  }\n}\n\nfunction getKeysFromPath(path: string, catalogData: unknown): string[] {\n  const parts = path.split(\".\");\n  let current: unknown = { catalog: catalogData };\n  for (const part of parts) {\n    if (current && typeof current === \"object\") {\n      current = (current as Record<string, unknown>)[part];\n    } else {\n      return [];\n    }\n  }\n  if (current && typeof current === \"object\") {\n    return Object.keys(current);\n  }\n  return [];\n}\n\nfunction getPropsFromPath(path: string, catalogData: unknown): z.ZodType[] {\n  const parts = path.split(\".\");\n  let current: unknown = { catalog: catalogData };\n  for (const part of parts) {\n    if (current && typeof current === \"object\") {\n      current = (current as Record<string, unknown>)[part];\n    } else {\n      return [];\n    }\n  }\n  if (current && typeof current === \"object\") {\n    return Object.values(current as Record<string, { props?: z.ZodType }>)\n      .map((entry) => entry.props)\n      .filter((props): props is z.ZodType => props !== undefined);\n  }\n  return [];\n}\n\n/**\n * Generate system prompt from catalog\n */\nfunction generatePrompt<TDef extends SchemaDefinition, TCatalog>(\n  catalog: Catalog<TDef, TCatalog>,\n  options: PromptOptions,\n): string {\n  // Check if schema has a custom prompt template\n  if (catalog.schema.promptTemplate) {\n    const context: PromptContext<TCatalog> = {\n      catalog: catalog.data,\n      componentNames: catalog.componentNames,\n      actionNames: catalog.actionNames,\n      options,\n      formatZodType,\n    };\n    return catalog.schema.promptTemplate(context);\n  }\n\n  // Default JSONL element-tree format (for @json-render/react and similar)\n  const {\n    system = \"You are a UI generator that outputs JSON.\",\n    customRules = [],\n    mode: rawMode = \"standalone\",\n  } = options;\n\n  const mode: \"standalone\" | \"inline\" =\n    rawMode === \"chat\"\n      ? (console.warn(\n          '[json-render] mode \"chat\" is deprecated, use \"inline\" instead',\n        ),\n        \"inline\")\n      : rawMode === \"generate\"\n        ? (console.warn(\n            '[json-render] mode \"generate\" is deprecated, use \"standalone\" instead',\n          ),\n          \"standalone\")\n        : rawMode;\n\n  const lines: string[] = [];\n  lines.push(system);\n  lines.push(\"\");\n\n  // Output format section - explain JSONL streaming patch format\n  if (mode === \"inline\") {\n    lines.push(\"OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):\");\n    lines.push(\n      \"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence.\",\n    );\n    lines.push(\n      \"The JSONL lines use RFC 6902 JSON Patch operations to build a UI tree. Always wrap them in a ```spec fence block:\",\n    );\n    lines.push(\"  ```spec\");\n    lines.push('  {\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}');\n    lines.push(\n      '  {\"op\":\"add\",\"path\":\"/elements/main\",\"value\":{\"type\":\"Card\",\"props\":{\"title\":\"Hello\"},\"children\":[]}}',\n    );\n    lines.push(\"  ```\");\n    lines.push(\n      \"If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only — no JSONL.\",\n    );\n  } else {\n    lines.push(\"OUTPUT FORMAT (JSONL, RFC 6902 JSON Patch):\");\n    lines.push(\n      \"Output JSONL (one JSON object per line) using RFC 6902 JSON Patch operations to build a UI tree.\",\n    );\n  }\n  lines.push(\n    \"Each line is a JSON patch operation (add, remove, replace). Start with /root, then stream /elements and /state patches interleaved so the UI fills in progressively as it streams.\",\n  );\n  lines.push(\"\");\n  lines.push(\"Example output (each line is a separate JSON object):\");\n  lines.push(\"\");\n\n  // Build example using actual catalog component names and props to avoid hallucinations\n  const allComponents = (catalog.data as Record<string, unknown>).components as\n    | Record<string, CatalogComponentDef>\n    | undefined;\n  const cn = catalog.componentNames;\n  const comp1 = cn[0] || \"Component\";\n  const comp2 = cn.length > 1 ? cn[1]! : comp1;\n  const comp1Def = allComponents?.[comp1];\n  const comp2Def = allComponents?.[comp2];\n  const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};\n  const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};\n\n  // Find a string prop on comp2 to demonstrate $state dynamic bindings\n  const dynamicPropName = comp2Def?.props\n    ? findFirstStringProp(comp2Def.props)\n    : null;\n  const dynamicProps = dynamicPropName\n    ? { ...comp2Props, [dynamicPropName]: { $item: \"title\" } }\n    : comp2Props;\n\n  const exampleOutput = [\n    JSON.stringify({ op: \"add\", path: \"/root\", value: \"main\" }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/elements/main\",\n      value: {\n        type: comp1,\n        props: comp1Props,\n        children: [\"child-1\", \"list\"],\n      },\n    }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/elements/child-1\",\n      value: { type: comp2, props: comp2Props, children: [] },\n    }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/elements/list\",\n      value: {\n        type: comp1,\n        props: comp1Props,\n        repeat: { statePath: \"/items\", key: \"id\" },\n        children: [\"item\"],\n      },\n    }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/elements/item\",\n      value: { type: comp2, props: dynamicProps, children: [] },\n    }),\n    JSON.stringify({ op: \"add\", path: \"/state/items\", value: [] }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/state/items/0\",\n      value: { id: \"1\", title: \"First Item\" },\n    }),\n    JSON.stringify({\n      op: \"add\",\n      path: \"/state/items/1\",\n      value: { id: \"2\", title: \"Second Item\" },\n    }),\n  ].join(\"\\n\");\n\n  lines.push(`${exampleOutput}\n\nNote: state patches appear right after the elements that use them, so the UI fills in as it streams. ONLY use component types from the AVAILABLE COMPONENTS list below.`);\n  lines.push(\"\");\n\n  // Initial state section\n  lines.push(\"INITIAL STATE:\");\n  lines.push(\n    \"Specs include a /state field to seed the state model. Components with { $bindState } or { $bindItem } read from and write to this state, and $state expressions read from it.\",\n  );\n  lines.push(\n    \"CRITICAL: You MUST include state patches whenever your UI displays data via $state, $bindState, $bindItem, $item, or $index expressions, or uses repeat to iterate over arrays. Without state, these references resolve to nothing and repeat lists render zero items.\",\n  );\n  lines.push(\n    \"Output state patches right after the elements that reference them, so the UI fills in progressively as it streams.\",\n  );\n  lines.push(\n    \"Stream state progressively - output one patch per array item instead of one giant blob:\",\n  );\n  lines.push(\n    '  For arrays: {\"op\":\"add\",\"path\":\"/state/posts/0\",\"value\":{\"id\":\"1\",\"title\":\"First Post\",...}} then /state/posts/1, /state/posts/2, etc.',\n  );\n  lines.push(\n    '  For scalars: {\"op\":\"add\",\"path\":\"/state/newTodoText\",\"value\":\"\"}',\n  );\n  lines.push(\n    '  Initialize the array first if needed: {\"op\":\"add\",\"path\":\"/state/posts\",\"value\":[]}',\n  );\n  lines.push(\n    'When content comes from the state model, use { \"$state\": \"/some/path\" } dynamic props to display it instead of hardcoding the same value in both state and props. The state model is the single source of truth.',\n  );\n  lines.push(\n    \"Include realistic sample data in state. For blogs: 3-4 posts with titles, excerpts, authors, dates. For product lists: 3-5 items with names, prices, descriptions. Never leave arrays empty.\",\n  );\n  lines.push(\"\");\n  lines.push(\"DYNAMIC LISTS (repeat field):\");\n  lines.push(\n    'Any element can have a top-level \"repeat\" field to render its children once per item in a state array: { \"repeat\": { \"statePath\": \"/arrayPath\", \"key\": \"id\" } }.',\n  );\n  lines.push(\n    'The element itself renders once (as the container), and its children are expanded once per array item. \"statePath\" is the state array path. \"key\" is an optional field name on each item for stable React keys.',\n  );\n  lines.push(\n    `Example: ${JSON.stringify({ type: comp1, props: comp1Props, repeat: { statePath: \"/todos\", key: \"id\" }, children: [\"todo-item\"] })}`,\n  );\n  lines.push(\n    'Inside children of a repeated element, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } to get the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" } on the appropriate prop.',\n  );\n  lines.push(\n    \"ALWAYS use the repeat field for lists backed by state arrays. NEVER hardcode individual elements for each array item.\",\n  );\n  lines.push(\n    'IMPORTANT: \"repeat\" is a top-level field on the element (sibling of type/props/children), NOT inside props.',\n  );\n  lines.push(\"\");\n  lines.push(\"ARRAY STATE ACTIONS:\");\n  lines.push(\n    'Use action \"pushState\" to append items to arrays. Params: { statePath: \"/arrayPath\", value: { ...item }, clearStatePath: \"/inputPath\" }.',\n  );\n  lines.push(\n    'Values inside pushState can contain { \"$state\": \"/statePath\" } references to read current state (e.g. the text from an input field).',\n  );\n  lines.push(\n    'Use \"$id\" inside a pushState value to auto-generate a unique ID.',\n  );\n  lines.push(\n    'Example: on: { \"press\": { \"action\": \"pushState\", \"params\": { \"statePath\": \"/todos\", \"value\": { \"id\": \"$id\", \"title\": { \"$state\": \"/newTodoText\" }, \"completed\": false }, \"clearStatePath\": \"/newTodoText\" } } }',\n  );\n  lines.push(\n    'Use action \"removeState\" to remove items from arrays by index. Params: { statePath: \"/arrayPath\", index: N }. Inside a repeated element\\'s children, use { \"$index\": true } for the current item index. Action params support the same expressions as props: { \"$item\": \"field\" } resolves to the absolute state path, { \"$index\": true } resolves to the index number, and { \"$state\": \"/path\" } reads a value from state.',\n  );\n  lines.push(\n    \"For lists where users can add/remove items (todos, carts, etc.), use pushState and removeState instead of hardcoding with setState.\",\n  );\n  lines.push(\"\");\n  lines.push(\n    'IMPORTANT: State paths use RFC 6901 JSON Pointer syntax (e.g. \"/todos/0/title\"). Do NOT use JavaScript-style dot notation (e.g. \"/todos.length\" is WRONG). To generate unique IDs for new items, use \"$id\" instead of trying to read array length.',\n  );\n  lines.push(\"\");\n\n  // Components section — reuse the typed reference from example generation\n  const components = allComponents;\n\n  if (components) {\n    lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);\n    lines.push(\"\");\n\n    for (const [name, def] of Object.entries(components)) {\n      const propsStr = def.props ? formatZodType(def.props) : \"{}\";\n      const hasChildren = def.slots && def.slots.length > 0;\n      const childrenStr = hasChildren ? \" [accepts children]\" : \"\";\n      const eventsStr =\n        def.events && def.events.length > 0\n          ? ` [events: ${def.events.join(\", \")}]`\n          : \"\";\n      const descStr = def.description ? ` - ${def.description}` : \"\";\n      lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);\n    }\n    lines.push(\"\");\n  }\n\n  // Actions section\n  const actions = (catalog.data as Record<string, unknown>).actions as\n    | Record<string, { params?: z.ZodType; description?: string }>\n    | undefined;\n\n  const builtInActions = catalog.schema.builtInActions ?? [];\n  const hasCustomActions = actions && catalog.actionNames.length > 0;\n  const hasBuiltInActions = builtInActions.length > 0;\n\n  if (hasCustomActions || hasBuiltInActions) {\n    lines.push(\"AVAILABLE ACTIONS:\");\n    lines.push(\"\");\n\n    // Built-in actions (handled by runtime, always available)\n    for (const action of builtInActions) {\n      lines.push(`- ${action.name}: ${action.description} [built-in]`);\n    }\n\n    // Custom actions (declared in catalog, require handlers)\n    if (hasCustomActions) {\n      for (const [name, def] of Object.entries(actions)) {\n        lines.push(`- ${name}${def.description ? `: ${def.description}` : \"\"}`);\n      }\n    }\n\n    lines.push(\"\");\n  }\n\n  // Events section\n  lines.push(\"EVENTS (the `on` field):\");\n  lines.push(\n    \"Elements can have an optional `on` field to bind events to actions. The `on` field is a top-level field on the element (sibling of type/props/children), NOT inside props.\",\n  );\n  lines.push(\n    'Each key in `on` is an event name (from the component\\'s supported events), and the value is an action binding: `{ \"action\": \"<actionName>\", \"params\": { ... } }`.',\n  );\n  lines.push(\"\");\n  lines.push(\"Example:\");\n  lines.push(\n    `  ${JSON.stringify({ type: comp1, props: comp1Props, on: { press: { action: \"setState\", params: { statePath: \"/saved\", value: true } } }, children: [] })}`,\n  );\n  lines.push(\"\");\n  lines.push(\n    'Action params can use dynamic references to read from state: { \"$state\": \"/statePath\" }.',\n  );\n  lines.push(\n    \"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field for event bindings.\",\n  );\n  lines.push(\"\");\n\n  // Visibility conditions\n  lines.push(\"VISIBILITY CONDITIONS:\");\n  lines.push(\n    \"Elements can have an optional `visible` field to conditionally show/hide based on state. IMPORTANT: `visible` is a top-level field on the element object (sibling of type/props/children), NOT inside props.\",\n  );\n  lines.push(\n    `Correct: ${JSON.stringify({ type: comp1, props: comp1Props, visible: { $state: \"/activeTab\", eq: \"home\" }, children: [\"...\"] })}`,\n  );\n  lines.push(\n    '- `{ \"$state\": \"/path\" }` - visible when state at path is truthy',\n  );\n  lines.push(\n    '- `{ \"$state\": \"/path\", \"not\": true }` - visible when state at path is falsy',\n  );\n  lines.push(\n    '- `{ \"$state\": \"/path\", \"eq\": \"value\" }` - visible when state equals value',\n  );\n  lines.push(\n    '- `{ \"$state\": \"/path\", \"neq\": \"value\" }` - visible when state does not equal value',\n  );\n  lines.push(\n    '- `{ \"$state\": \"/path\", \"gt\": N }` / `gte` / `lt` / `lte` - numeric comparisons',\n  );\n  lines.push(\n    \"- Use ONE operator per condition (eq, neq, gt, gte, lt, lte). Do not combine multiple operators.\",\n  );\n  lines.push('- Any condition can add `\"not\": true` to invert its result');\n  lines.push(\n    \"- `[condition, condition]` - all conditions must be true (implicit AND)\",\n  );\n  lines.push(\n    '- `{ \"$and\": [condition, condition] }` - explicit AND (use when nesting inside $or)',\n  );\n  lines.push(\n    '- `{ \"$or\": [condition, condition] }` - at least one must be true (OR)',\n  );\n  lines.push(\"- `true` / `false` - always visible/hidden\");\n  lines.push(\"\");\n  lines.push(\n    \"Use a component with on.press bound to setState to update state and drive visibility.\",\n  );\n  lines.push(\n    `Example: A ${comp1} with on: { \"press\": { \"action\": \"setState\", \"params\": { \"statePath\": \"/activeTab\", \"value\": \"home\" } } } sets state, then a container with visible: { \"$state\": \"/activeTab\", \"eq\": \"home\" } shows only when that tab is active.`,\n  );\n  lines.push(\"\");\n  lines.push(\n    'For tab patterns where the first/default tab should be visible when no tab is selected yet, use $or to handle both cases: visible: { \"$or\": [{ \"$state\": \"/activeTab\", \"eq\": \"home\" }, { \"$state\": \"/activeTab\", \"not\": true }] }. This ensures the first tab is visible both when explicitly selected AND when /activeTab is not yet set.',\n  );\n  lines.push(\"\");\n\n  // Dynamic prop expressions\n  lines.push(\"DYNAMIC PROPS:\");\n  lines.push(\n    \"Any prop value can be a dynamic expression that resolves based on state. Three forms are supported:\",\n  );\n  lines.push(\"\");\n  lines.push(\n    '1. Read-only state: `{ \"$state\": \"/statePath\" }` - resolves to the value at that state path (one-way read).',\n  );\n  lines.push(\n    '   Example: `\"color\": { \"$state\": \"/theme/primary\" }` reads the color from state.',\n  );\n  lines.push(\"\");\n  lines.push(\n    '2. Two-way binding: `{ \"$bindState\": \"/statePath\" }` - resolves to the value at the state path AND enables write-back. Use on form input props (value, checked, pressed, etc.).',\n  );\n  lines.push(\n    '   Example: `\"value\": { \"$bindState\": \"/form/email\" }` binds the input value to /form/email.',\n  );\n  lines.push(\n    '   Inside repeat scopes: `\"checked\": { \"$bindItem\": \"completed\" }` binds to the current item\\'s completed field.',\n  );\n  lines.push(\"\");\n  lines.push(\n    '3. Conditional: `{ \"$cond\": <condition>, \"$then\": <value>, \"$else\": <value> }` - evaluates the condition (same syntax as visibility conditions) and picks the matching value.',\n  );\n  lines.push(\n    '   Example: `\"color\": { \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" }, \"$then\": \"#007AFF\", \"$else\": \"#8E8E93\" }`',\n  );\n  lines.push(\"\");\n  lines.push(\n    \"Use $bindState for form inputs (text fields, checkboxes, selects, sliders, etc.) and $state for read-only data display. Inside repeat scopes, use $bindItem for form inputs bound to the current item. Use dynamic props instead of duplicating elements with opposing visible conditions when only prop values differ.\",\n  );\n  lines.push(\"\");\n  lines.push(\n    '4. Template: `{ \"$template\": \"Hello, ${/name}!\" }` - interpolates `${/path}` references in the string with values from the state model.',\n  );\n  lines.push(\n    '   Example: `\"label\": { \"$template\": \"Items: ${/cart/count} | Total: ${/cart/total}\" }` renders \"Items: 3 | Total: 42.00\" when /cart/count is 3 and /cart/total is 42.00.',\n  );\n  lines.push(\"\");\n\n  // $computed section — only emit when catalog defines functions\n  const catalogFunctions = (catalog.data as Record<string, unknown>).functions;\n  if (catalogFunctions && Object.keys(catalogFunctions).length > 0) {\n    lines.push(\n      '5. Computed: `{ \"$computed\": \"<functionName>\", \"args\": { \"key\": <expression> } }` - calls a registered function with resolved args and returns the result.',\n    );\n    lines.push(\n      '   Example: `\"value\": { \"$computed\": \"fullName\", \"args\": { \"first\": { \"$state\": \"/form/firstName\" }, \"last\": { \"$state\": \"/form/lastName\" } } }`',\n    );\n    lines.push(\"   Available functions:\");\n    for (const name of Object.keys(\n      catalogFunctions as Record<string, unknown>,\n    )) {\n      lines.push(`   - ${name}`);\n    }\n    lines.push(\"\");\n  }\n\n  // Validation section — only emit when at least one component has a `checks` prop\n  const hasChecksComponents = allComponents\n    ? Object.entries(allComponents).some(([, def]) => {\n        if (!def.props) return false;\n        const formatted = formatZodType(def.props);\n        return formatted.includes(\"checks\");\n      })\n    : false;\n\n  if (hasChecksComponents) {\n    lines.push(\"VALIDATION:\");\n    lines.push(\n      \"Form components that accept a `checks` prop support client-side validation.\",\n    );\n    lines.push(\n      'Each check is an object: { \"type\": \"<name>\", \"message\": \"...\", \"args\": { ... } }',\n    );\n    lines.push(\"\");\n    lines.push(\"Built-in validation types:\");\n    lines.push(\"  - required — value must be non-empty\");\n    lines.push(\"  - email — valid email format\");\n    lines.push('  - minLength — minimum string length (args: { \"min\": N })');\n    lines.push('  - maxLength — maximum string length (args: { \"max\": N })');\n    lines.push('  - pattern — match a regex (args: { \"pattern\": \"regex\" })');\n    lines.push('  - min — minimum numeric value (args: { \"min\": N })');\n    lines.push('  - max — maximum numeric value (args: { \"max\": N })');\n    lines.push(\"  - numeric — value must be a number\");\n    lines.push(\"  - url — valid URL format\");\n    lines.push(\n      '  - matches — must equal another field (args: { \"other\": { \"$state\": \"/path\" } })',\n    );\n    lines.push(\n      '  - equalTo — alias for matches (args: { \"other\": { \"$state\": \"/path\" } })',\n    );\n    lines.push(\n      '  - lessThan — value must be less than another field (args: { \"other\": { \"$state\": \"/path\" } })',\n    );\n    lines.push(\n      '  - greaterThan — value must be greater than another field (args: { \"other\": { \"$state\": \"/path\" } })',\n    );\n    lines.push(\n      '  - requiredIf — required only when another field is truthy (args: { \"field\": { \"$state\": \"/path\" } })',\n    );\n    lines.push(\"\");\n    lines.push(\"Example:\");\n    lines.push(\n      '  \"checks\": [{ \"type\": \"required\", \"message\": \"Email is required\" }, { \"type\": \"email\", \"message\": \"Invalid email\" }]',\n    );\n    lines.push(\"\");\n    lines.push(\n      \"IMPORTANT: When using checks, the component must also have a { $bindState } or { $bindItem } on its value/checked prop for two-way binding.\",\n    );\n    lines.push(\n      \"Always include validation checks on form inputs for a good user experience (e.g. required, email, minLength).\",\n    );\n    lines.push(\"\");\n  }\n\n  // State watchers section — only emit when actions are available (watchers\n  // trigger actions, so the section is irrelevant without them).\n  if (hasCustomActions || hasBuiltInActions) {\n    lines.push(\"STATE WATCHERS:\");\n    lines.push(\n      \"Elements can have an optional `watch` field to react to state changes and trigger actions. The `watch` field is a top-level field on the element (sibling of type/props/children), NOT inside props.\",\n    );\n    lines.push(\n      \"Maps state paths (JSON Pointers) to action bindings. When the value at a watched path changes, the bound actions fire automatically.\",\n    );\n    lines.push(\"\");\n    lines.push(\n      \"Example (cascading select — country changes trigger city loading):\",\n    );\n    lines.push(\n      `  ${JSON.stringify({ type: \"Select\", props: { value: { $bindState: \"/form/country\" }, options: [\"US\", \"Canada\", \"UK\"] }, watch: { \"/form/country\": { action: \"loadCities\", params: { country: { $state: \"/form/country\" } } } }, children: [] })}`,\n    );\n    lines.push(\"\");\n    lines.push(\n      \"Use `watch` for cascading dependencies where changing one field should trigger side effects (loading data, resetting dependent fields, computing derived values).\",\n    );\n    lines.push(\n      \"IMPORTANT: `watch` is a top-level field on the element (sibling of type/props/children), NOT inside props. Watchers only fire when the value changes, not on initial render.\",\n    );\n    lines.push(\"\");\n  }\n\n  // Edit modes\n  const editModes = options.editModes;\n  if (editModes && editModes.length > 0) {\n    lines.push(buildEditInstructions({ modes: editModes }, \"json\"));\n  }\n\n  // Rules\n  lines.push(\"RULES:\");\n  const baseRules =\n    mode === \"inline\"\n      ? [\n          \"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence\",\n          \"Write a brief conversational response before any JSONL output\",\n          'First set root: {\"op\":\"add\",\"path\":\"/root\",\"value\":\"<root-key>\"}',\n          'Then add each element: {\"op\":\"add\",\"path\":\"/elements/<key>\",\"value\":{...}}',\n          \"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.\",\n          \"ONLY use components listed above\",\n          \"Each element value needs: type, props, children (array of child keys)\",\n          \"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')\",\n        ]\n      : [\n          \"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences\",\n          'First set root: {\"op\":\"add\",\"path\":\"/root\",\"value\":\"<root-key>\"}',\n          'Then add each element: {\"op\":\"add\",\"path\":\"/elements/<key>\",\"value\":{...}}',\n          \"Output /state patches right after the elements that use them, one per array item for progressive loading. REQUIRED whenever using $state, $bindState, $bindItem, $item, $index, or repeat.\",\n          \"ONLY use components listed above\",\n          \"Each element value needs: type, props, children (array of child keys)\",\n          \"Use unique keys for the element map entries (e.g., 'header', 'metric-1', 'chart-revenue')\",\n        ];\n  const schemaRules = catalog.schema.defaultRules ?? [];\n  const allRules = [...baseRules, ...schemaRules, ...customRules];\n  allRules.forEach((rule, i) => {\n    lines.push(`${i + 1}. ${rule}`);\n  });\n\n  return lines.join(\"\\n\");\n}\n\n// =============================================================================\n// Example Value Generation from Zod Schemas\n// =============================================================================\n\n/**\n * Component definition shape as it appears in catalog data\n */\ninterface CatalogComponentDef {\n  props?: z.ZodType;\n  description?: string;\n  slots?: string[];\n  events?: string[];\n  example?: Record<string, unknown>;\n}\n\n/**\n * Get example props for a catalog component.\n * Uses the explicit `example` field if provided, otherwise generates from Zod schema.\n */\nfunction getExampleProps(def: CatalogComponentDef): Record<string, unknown> {\n  if (def.example && Object.keys(def.example).length > 0) {\n    return def.example;\n  }\n  if (def.props) {\n    return generateExamplePropsFromZod(def.props);\n  }\n  return {};\n}\n\n/**\n * Generate example prop values from a Zod object schema.\n * Only includes required fields to keep examples concise.\n */\nfunction generateExamplePropsFromZod(\n  schema: z.ZodType,\n): Record<string, unknown> {\n  if (!schema || !schema._def) return {};\n  const def = schema._def as unknown as Record<string, unknown>;\n  const typeName = getZodTypeName(schema);\n\n  if (typeName !== \"ZodObject\" && typeName !== \"object\") return {};\n\n  const shape =\n    typeof def.shape === \"function\"\n      ? (def.shape as () => Record<string, z.ZodType>)()\n      : (def.shape as Record<string, z.ZodType>);\n  if (!shape) return {};\n\n  const result: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(shape)) {\n    const innerTypeName = getZodTypeName(value);\n    // Skip optional props to keep examples concise\n    if (\n      innerTypeName === \"ZodOptional\" ||\n      innerTypeName === \"optional\" ||\n      innerTypeName === \"ZodNullable\" ||\n      innerTypeName === \"nullable\"\n    ) {\n      continue;\n    }\n    result[key] = generateExampleValue(value);\n  }\n  return result;\n}\n\n/**\n * Generate a single example value from a Zod type.\n */\nfunction generateExampleValue(schema: z.ZodType): unknown {\n  if (!schema || !schema._def) return \"...\";\n  const def = schema._def as unknown as Record<string, unknown>;\n  const typeName = getZodTypeName(schema);\n\n  switch (typeName) {\n    case \"ZodString\":\n    case \"string\":\n      return \"example\";\n    case \"ZodNumber\":\n    case \"number\":\n      return 0;\n    case \"ZodBoolean\":\n    case \"boolean\":\n      return true;\n    case \"ZodLiteral\":\n    case \"literal\":\n      return def.value;\n    case \"ZodEnum\":\n    case \"enum\": {\n      if (Array.isArray(def.values) && def.values.length > 0)\n        return def.values[0];\n      if (def.entries && typeof def.entries === \"object\") {\n        const values = Object.values(def.entries as Record<string, string>);\n        return values.length > 0 ? values[0] : \"example\";\n      }\n      return \"example\";\n    }\n    case \"ZodOptional\":\n    case \"optional\":\n    case \"ZodNullable\":\n    case \"nullable\":\n    case \"ZodDefault\":\n    case \"default\": {\n      const inner = (def.innerType as z.ZodType) ?? (def.wrapped as z.ZodType);\n      return inner ? generateExampleValue(inner) : null;\n    }\n    case \"ZodArray\":\n    case \"array\":\n      return [];\n    case \"ZodObject\":\n    case \"object\":\n      return generateExamplePropsFromZod(schema);\n    case \"ZodUnion\":\n    case \"union\": {\n      const options = def.options as z.ZodType[] | undefined;\n      return options && options.length > 0\n        ? generateExampleValue(options[0]!)\n        : \"...\";\n    }\n    default:\n      return \"...\";\n  }\n}\n\n/**\n * Find the name of the first required string prop in a Zod object schema.\n * Used to demonstrate $state dynamic bindings in examples.\n */\nfunction findFirstStringProp(schema?: z.ZodType): string | null {\n  if (!schema || !schema._def) return null;\n  const def = schema._def as unknown as Record<string, unknown>;\n  const typeName = getZodTypeName(schema);\n\n  if (typeName !== \"ZodObject\" && typeName !== \"object\") return null;\n\n  const shape =\n    typeof def.shape === \"function\"\n      ? (def.shape as () => Record<string, z.ZodType>)()\n      : (def.shape as Record<string, z.ZodType>);\n  if (!shape) return null;\n\n  for (const [key, value] of Object.entries(shape)) {\n    const innerTypeName = getZodTypeName(value);\n    // Skip optional props\n    if (\n      innerTypeName === \"ZodOptional\" ||\n      innerTypeName === \"optional\" ||\n      innerTypeName === \"ZodNullable\" ||\n      innerTypeName === \"nullable\"\n    ) {\n      continue;\n    }\n    // Unwrap to check the actual type\n    if (innerTypeName === \"ZodString\" || innerTypeName === \"string\") {\n      return key;\n    }\n  }\n  return null;\n}\n\n// =============================================================================\n// Zod Introspection Helpers\n// =============================================================================\n\n/**\n * Get Zod type name from schema (handles different Zod versions)\n */\nfunction getZodTypeName(schema: z.ZodType): string {\n  if (!schema || !schema._def) return \"\";\n  const def = schema._def as unknown as Record<string, unknown>;\n  // Zod 4+ uses _def.type, older versions use _def.typeName\n  return (def.typeName as string) ?? (def.type as string) ?? \"\";\n}\n\n/**\n * Format a Zod type into a human-readable string\n */\nfunction formatZodType(schema: z.ZodType): string {\n  if (!schema || !schema._def) return \"unknown\";\n  const def = schema._def as unknown as Record<string, unknown>;\n  const typeName = getZodTypeName(schema);\n\n  switch (typeName) {\n    case \"ZodString\":\n    case \"string\":\n      return \"string\";\n    case \"ZodNumber\":\n    case \"number\":\n      return \"number\";\n    case \"ZodBoolean\":\n    case \"boolean\":\n      return \"boolean\";\n    case \"ZodLiteral\":\n    case \"literal\":\n      return JSON.stringify(def.value);\n    case \"ZodEnum\":\n    case \"enum\": {\n      // Zod 3 uses values array, Zod 4 uses entries object\n      let values: string[];\n      if (Array.isArray(def.values)) {\n        values = def.values as string[];\n      } else if (def.entries && typeof def.entries === \"object\") {\n        values = Object.values(def.entries as Record<string, string>);\n      } else {\n        return \"enum\";\n      }\n      return values.map((v) => `\"${v}\"`).join(\" | \");\n    }\n    case \"ZodArray\":\n    case \"array\": {\n      // safely resolve inner type for Zod arrays\n      const inner = (\n        typeof def.element === \"object\"\n          ? def.element\n          : typeof def.type === \"object\"\n            ? def.type\n            : undefined\n      ) as z.ZodType | undefined;\n      return inner ? `Array<${formatZodType(inner)}>` : \"Array<unknown>\";\n    }\n    case \"ZodObject\":\n    case \"object\": {\n      // Shape can be a function (Zod 3) or direct object (Zod 4)\n      const shape =\n        typeof def.shape === \"function\"\n          ? (def.shape as () => Record<string, z.ZodType>)()\n          : (def.shape as Record<string, z.ZodType>);\n      if (!shape) return \"object\";\n      const props = Object.entries(shape)\n        .map(([key, value]) => {\n          const innerTypeName = getZodTypeName(value);\n          const isOptional =\n            innerTypeName === \"ZodOptional\" ||\n            innerTypeName === \"ZodNullable\" ||\n            innerTypeName === \"optional\" ||\n            innerTypeName === \"nullable\";\n          return `${key}${isOptional ? \"?\" : \"\"}: ${formatZodType(value)}`;\n        })\n        .join(\", \");\n      return `{ ${props} }`;\n    }\n    case \"ZodOptional\":\n    case \"optional\":\n    case \"ZodNullable\":\n    case \"nullable\": {\n      const inner = (def.innerType as z.ZodType) ?? (def.wrapped as z.ZodType);\n      return inner ? formatZodType(inner) : \"unknown\";\n    }\n    case \"ZodUnion\":\n    case \"union\": {\n      const options = def.options as z.ZodType[] | undefined;\n      return options\n        ? options.map((opt) => formatZodType(opt)).join(\" | \")\n        : \"unknown\";\n    }\n    default:\n      return \"unknown\";\n  }\n}\n\n/**\n * Resolve the Zod type name from a schema's internal definition.\n * Supports both Zod 3 (`_def.typeName`) and Zod 4 (`_def.type`).\n */\nfunction zodTypeName(def: Record<string, unknown>): string {\n  // Zod 4 uses _def.type as a plain string (e.g. \"string\", \"object\")\n  if (typeof def.type === \"string\") return def.type;\n  // Zod 3 uses _def.typeName (e.g. \"ZodString\", \"ZodObject\")\n  if (typeof def.typeName === \"string\") return def.typeName;\n  return \"\";\n}\n\n/**\n * Normalise a Zod type name to a canonical lowercase form.\n * Handles both Zod 3 (\"ZodString\") and Zod 4 (\"string\") conventions.\n */\nfunction normalizeTypeName(raw: string): string {\n  // Zod 3 names start with \"Zod\", e.g. \"ZodString\" → \"string\"\n  if (raw.startsWith(\"Zod\")) {\n    return raw.slice(3).toLowerCase();\n  }\n  return raw.toLowerCase();\n}\n\n/**\n * Convert Zod schema to JSON Schema.\n *\n * When `strict` is true the output conforms to the JSON Schema subset required\n * by LLM structured output APIs (no `propertyNames`, `additionalProperties: false`\n * everywhere, all properties listed in `required`).\n */\nfunction zodToJsonSchema(schema: z.ZodType, strict = false): object {\n  const def = schema._def as unknown as Record<string, unknown>;\n  const kind = normalizeTypeName(zodTypeName(def));\n\n  switch (kind) {\n    case \"string\":\n      return { type: \"string\" };\n    case \"number\":\n      return { type: \"number\" };\n    case \"boolean\":\n      return { type: \"boolean\" };\n    case \"literal\": {\n      // Zod 4: _def.values (array), Zod 3: _def.value (single)\n      const values = def.values as unknown[] | undefined;\n      const value = values ? values[0] : def.value;\n      return { const: value };\n    }\n    case \"enum\": {\n      // Zod 4: _def.entries (object { a:\"a\", b:\"b\" }), Zod 3: _def.values (string[])\n      const entries = def.entries as Record<string, string> | undefined;\n      const values = entries\n        ? Object.values(entries)\n        : (def.values as string[] | undefined);\n      return { enum: values ?? [] };\n    }\n    case \"array\": {\n      // Zod 4: _def.element, Zod 3: _def.type\n      const inner = (def.element ?? def.type) as z.ZodType | undefined;\n      return {\n        type: \"array\",\n        items: inner ? zodToJsonSchema(inner, strict) : {},\n      };\n    }\n    case \"object\": {\n      // Zod 4: _def.shape is an object, Zod 3: _def.shape is a function\n      const rawShape = def.shape;\n      const shape: Record<string, z.ZodType> | undefined =\n        typeof rawShape === \"function\"\n          ? (rawShape as () => Record<string, z.ZodType>)()\n          : (rawShape as Record<string, z.ZodType> | undefined);\n\n      if (!shape) {\n        if (strict) {\n          return {\n            type: \"object\",\n            properties: {},\n            required: [],\n            additionalProperties: false,\n          };\n        }\n        return { type: \"object\" };\n      }\n\n      const properties: Record<string, object> = {};\n      const required: string[] = [];\n      for (const [key, value] of Object.entries(shape)) {\n        const innerDef = value._def as unknown as Record<string, unknown>;\n        const innerKind = normalizeTypeName(zodTypeName(innerDef));\n        const isOptional = innerKind === \"optional\" || innerKind === \"nullable\";\n\n        if (strict) {\n          // In strict mode, all properties must be in required.\n          // Optional properties are represented as nullable types.\n          required.push(key);\n          if (isOptional) {\n            const unwrapped = zodToJsonSchema(value, strict);\n            properties[key] = { anyOf: [unwrapped, { type: \"null\" }] };\n          } else {\n            properties[key] = zodToJsonSchema(value, strict);\n          }\n        } else {\n          properties[key] = zodToJsonSchema(value);\n          if (!isOptional) {\n            required.push(key);\n          }\n        }\n      }\n      return {\n        type: \"object\",\n        properties,\n        required: required.length > 0 ? required : undefined,\n        additionalProperties: false,\n      };\n    }\n    case \"record\": {\n      const valueType = def.valueType as z.ZodType | undefined;\n      if (strict) {\n        // LLM strict schemas require `additionalProperties: false` and do not\n        // permit a schema value for `additionalProperties`. Since record types\n        // have dynamic keys that cannot be enumerated at schema-generation time,\n        // we emit an opaque object. The LLM prompt still describes the expected\n        // structure so the model can produce valid output.\n        return {\n          type: \"object\",\n          properties: {},\n          required: [],\n          additionalProperties: false,\n        };\n      }\n      return {\n        type: \"object\",\n        additionalProperties: valueType ? zodToJsonSchema(valueType) : true,\n      };\n    }\n    case \"optional\":\n    case \"nullable\": {\n      const inner = def.innerType as z.ZodType | undefined;\n      return inner ? zodToJsonSchema(inner, strict) : {};\n    }\n    case \"union\": {\n      const options = def.options as z.ZodType[] | undefined;\n      return options\n        ? { anyOf: options.map((o) => zodToJsonSchema(o, strict)) }\n        : {};\n    }\n    case \"any\":\n    case \"unknown\":\n      if (strict) {\n        return {\n          type: \"object\",\n          properties: {},\n          required: [],\n          additionalProperties: false,\n        };\n      }\n      return {};\n    default:\n      return {};\n  }\n}\n\n/**\n * Shorthand: Define a catalog directly from a schema\n */\nexport function defineCatalog<\n  TDef extends SchemaDefinition,\n  TCatalog extends InferCatalogInput<TDef[\"catalog\"]>,\n>(schema: Schema<TDef>, catalog: TCatalog): Catalog<TDef, TCatalog> {\n  return schema.createCatalog(catalog);\n}\n"
  },
  {
    "path": "packages/core/src/spec-validator.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport type { Spec } from \"./types\";\nimport { validateSpec, autoFixSpec } from \"./spec-validator\";\n\n// =============================================================================\n// validateSpec\n// =============================================================================\n\ndescribe(\"validateSpec\", () => {\n  it(\"returns valid for a correct spec\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: { type: \"Stack\", props: {}, children: [\"child1\"] },\n        child1: { type: \"Text\", props: { text: \"hello\" }, children: [] },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(true);\n    expect(result.issues).toHaveLength(0);\n  });\n\n  it(\"detects missing root\", () => {\n    const spec = {\n      root: \"\",\n      elements: { a: { type: \"T\", props: {}, children: [] } },\n    } as Spec;\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"missing_root\")).toBe(true);\n  });\n\n  it(\"detects root_not_found\", () => {\n    const spec: Spec = {\n      root: \"missing\",\n      elements: { a: { type: \"T\", props: {}, children: [] } },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"root_not_found\")).toBe(true);\n  });\n\n  it(\"detects empty spec\", () => {\n    const spec: Spec = { root: \"r\", elements: {} };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"empty_spec\")).toBe(true);\n  });\n\n  it(\"detects missing_child\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: { type: \"Stack\", props: {}, children: [\"nonexistent\"] },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"missing_child\")).toBe(true);\n  });\n\n  it(\"detects visible_in_props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Text\",\n          props: { visible: { $state: \"/show\" } },\n          children: [],\n        },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"visible_in_props\")).toBe(true);\n  });\n\n  it(\"detects on_in_props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: { on: { press: { action: \"doSomething\" } } },\n          children: [],\n        },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"on_in_props\")).toBe(true);\n  });\n\n  it(\"detects repeat_in_props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Stack\",\n          props: { repeat: { statePath: \"/items\" } },\n          children: [],\n        },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    expect(result.issues.some((i) => i.code === \"repeat_in_props\")).toBe(true);\n  });\n\n  it(\"detects watch_in_props\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Select\",\n          props: {\n            watch: {\n              \"/form/country\": { action: \"loadCities\" },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n    const result = validateSpec(spec);\n    expect(result.valid).toBe(false);\n    const watchIssue = result.issues.find((i) => i.code === \"watch_in_props\");\n    expect(watchIssue).toBeDefined();\n    expect(watchIssue!.elementKey).toBe(\"root\");\n  });\n\n  it(\"detects orphaned elements when checkOrphans is true\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: { type: \"Stack\", props: {}, children: [] },\n        orphan: { type: \"Text\", props: {}, children: [] },\n      },\n    };\n    const result = validateSpec(spec, { checkOrphans: true });\n    expect(result.valid).toBe(true);\n    expect(result.issues.some((i) => i.code === \"orphaned_element\")).toBe(true);\n  });\n});\n\n// =============================================================================\n// autoFixSpec\n// =============================================================================\n\ndescribe(\"autoFixSpec\", () => {\n  it(\"moves visible from props to element level\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Text\",\n          props: { text: \"hi\", visible: { $state: \"/show\" } },\n          children: [],\n        },\n      },\n    };\n    const { spec: fixed, fixes } = autoFixSpec(spec);\n    expect(\n      (fixed.elements.root.props as Record<string, unknown>).visible,\n    ).toBeUndefined();\n    expect(fixed.elements.root.visible).toEqual({ $state: \"/show\" });\n    expect(fixes.some((f) => f.includes(\"visible\"))).toBe(true);\n  });\n\n  it(\"moves on from props to element level\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Button\",\n          props: { label: \"OK\", on: { press: { action: \"submit\" } } },\n          children: [],\n        },\n      },\n    };\n    const { spec: fixed, fixes } = autoFixSpec(spec);\n    expect(\n      (fixed.elements.root.props as Record<string, unknown>).on,\n    ).toBeUndefined();\n    expect(fixed.elements.root.on).toEqual({ press: { action: \"submit\" } });\n    expect(fixes.some((f) => f.includes('\"on\"'))).toBe(true);\n  });\n\n  it(\"moves repeat from props to element level\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Stack\",\n          props: { repeat: { statePath: \"/items\" } },\n          children: [\"child\"],\n        },\n        child: { type: \"Text\", props: {}, children: [] },\n      },\n    };\n    const { spec: fixed, fixes } = autoFixSpec(spec);\n    expect(\n      (fixed.elements.root.props as Record<string, unknown>).repeat,\n    ).toBeUndefined();\n    expect(fixed.elements.root.repeat).toEqual({ statePath: \"/items\" });\n    expect(fixes.some((f) => f.includes('\"repeat\"'))).toBe(true);\n  });\n\n  it(\"moves watch from props to element level\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Select\",\n          props: {\n            label: \"Country\",\n            watch: {\n              \"/form/country\": { action: \"loadCities\" },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n    const { spec: fixed, fixes } = autoFixSpec(spec);\n    expect(\n      (fixed.elements.root.props as Record<string, unknown>).watch,\n    ).toBeUndefined();\n    expect(fixed.elements.root.watch).toEqual({\n      \"/form/country\": { action: \"loadCities\" },\n    });\n    expect(fixes.some((f) => f.includes('\"watch\"'))).toBe(true);\n  });\n\n  it(\"returns no fixes for a correct spec\", () => {\n    const spec: Spec = {\n      root: \"root\",\n      elements: {\n        root: {\n          type: \"Stack\",\n          props: { direction: \"vertical\" },\n          children: [],\n          watch: { \"/x\": { action: \"y\" } },\n        },\n      },\n    };\n    const { fixes } = autoFixSpec(spec);\n    expect(fixes).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/spec-validator.ts",
    "content": "import type { Spec, UIElement } from \"./types\";\n\n// =============================================================================\n// Spec Structural Validation\n// =============================================================================\n\n/**\n * Severity level for validation issues.\n */\nexport type SpecIssueSeverity = \"error\" | \"warning\";\n\n/**\n * A single validation issue found in a spec.\n */\nexport interface SpecIssue {\n  /** Severity: errors should be fixed, warnings are informational */\n  severity: SpecIssueSeverity;\n  /** Human-readable description of the issue */\n  message: string;\n  /** The element key where the issue was found (if applicable) */\n  elementKey?: string;\n  /** Machine-readable issue code for programmatic handling */\n  code:\n    | \"missing_root\"\n    | \"root_not_found\"\n    | \"missing_child\"\n    | \"visible_in_props\"\n    | \"orphaned_element\"\n    | \"empty_spec\"\n    | \"on_in_props\"\n    | \"repeat_in_props\"\n    | \"watch_in_props\";\n}\n\n/**\n * Result of spec structural validation.\n */\nexport interface SpecValidationIssues {\n  /** Whether the spec passed validation (no errors; warnings are OK) */\n  valid: boolean;\n  /** List of issues found */\n  issues: SpecIssue[];\n}\n\n/**\n * Options for validateSpec.\n */\nexport interface ValidateSpecOptions {\n  /**\n   * Whether to check for orphaned elements (elements not reachable from root).\n   * Defaults to false since orphans are harmless (just unused).\n   */\n  checkOrphans?: boolean;\n}\n\n/**\n * Validate a spec for structural integrity.\n *\n * Checks for common AI-generation errors:\n * - Missing or empty root\n * - Root element not found in elements map\n * - Children referencing non-existent elements\n * - `visible` placed inside `props` instead of on the element\n * - Orphaned elements (optional)\n *\n * @example\n * ```ts\n * const result = validateSpec(spec);\n * if (!result.valid) {\n *   console.log(\"Spec errors:\", result.issues);\n * }\n * ```\n */\nexport function validateSpec(\n  spec: Spec,\n  options: ValidateSpecOptions = {},\n): SpecValidationIssues {\n  const { checkOrphans = false } = options;\n  const issues: SpecIssue[] = [];\n\n  // 1. Check root\n  if (!spec.root) {\n    issues.push({\n      severity: \"error\",\n      message: \"Spec has no root element defined.\",\n      code: \"missing_root\",\n    });\n    return { valid: false, issues };\n  }\n\n  if (!spec.elements[spec.root]) {\n    issues.push({\n      severity: \"error\",\n      message: `Root element \"${spec.root}\" not found in elements map.`,\n      code: \"root_not_found\",\n    });\n  }\n\n  // 2. Check for empty spec\n  if (Object.keys(spec.elements).length === 0) {\n    issues.push({\n      severity: \"error\",\n      message: \"Spec has no elements.\",\n      code: \"empty_spec\",\n    });\n    return { valid: false, issues };\n  }\n\n  // 3. Check each element\n  for (const [key, element] of Object.entries(spec.elements)) {\n    // 3a. Missing children\n    if (element.children) {\n      for (const childKey of element.children) {\n        if (!spec.elements[childKey]) {\n          issues.push({\n            severity: \"error\",\n            message: `Element \"${key}\" references child \"${childKey}\" which does not exist in the elements map.`,\n            elementKey: key,\n            code: \"missing_child\",\n          });\n        }\n      }\n    }\n\n    // 3b. `visible` inside props\n    const props = element.props as Record<string, unknown> | undefined;\n    if (props && \"visible\" in props && props.visible !== undefined) {\n      issues.push({\n        severity: \"error\",\n        message: `Element \"${key}\" has \"visible\" inside \"props\". It should be a top-level field on the element (sibling of type/props/children).`,\n        elementKey: key,\n        code: \"visible_in_props\",\n      });\n    }\n\n    // 3c. `on` inside props (should be a top-level field)\n    if (props && \"on\" in props && props.on !== undefined) {\n      issues.push({\n        severity: \"error\",\n        message: `Element \"${key}\" has \"on\" inside \"props\". It should be a top-level field on the element (sibling of type/props/children).`,\n        elementKey: key,\n        code: \"on_in_props\",\n      });\n    }\n\n    // 3d. `repeat` inside props (should be a top-level field)\n    if (props && \"repeat\" in props && props.repeat !== undefined) {\n      issues.push({\n        severity: \"error\",\n        message: `Element \"${key}\" has \"repeat\" inside \"props\". It should be a top-level field on the element (sibling of type/props/children).`,\n        elementKey: key,\n        code: \"repeat_in_props\",\n      });\n    }\n\n    // 3e. `watch` inside props (should be a top-level field)\n    if (props && \"watch\" in props && props.watch !== undefined) {\n      issues.push({\n        severity: \"error\",\n        message: `Element \"${key}\" has \"watch\" inside \"props\". It should be a top-level field on the element (sibling of type/props/children).`,\n        elementKey: key,\n        code: \"watch_in_props\",\n      });\n    }\n  }\n\n  // 4. Orphaned elements (optional)\n  if (checkOrphans) {\n    const reachable = new Set<string>();\n    const walk = (key: string) => {\n      if (reachable.has(key)) return;\n      reachable.add(key);\n      const el = spec.elements[key];\n      if (el?.children) {\n        for (const childKey of el.children) {\n          if (spec.elements[childKey]) {\n            walk(childKey);\n          }\n        }\n      }\n    };\n    if (spec.elements[spec.root]) {\n      walk(spec.root);\n    }\n\n    for (const key of Object.keys(spec.elements)) {\n      if (!reachable.has(key)) {\n        issues.push({\n          severity: \"warning\",\n          message: `Element \"${key}\" is not reachable from root \"${spec.root}\".`,\n          elementKey: key,\n          code: \"orphaned_element\",\n        });\n      }\n    }\n  }\n\n  const hasErrors = issues.some((i) => i.severity === \"error\");\n  return { valid: !hasErrors, issues };\n}\n\n/**\n * Auto-fix common spec issues in-place and return a corrected copy.\n *\n * Currently fixes:\n * - `visible` inside `props` → moved to element level\n * - `on` inside `props` → moved to element level\n * - `repeat` inside `props` → moved to element level\n *\n * Returns the fixed spec and a list of fixes applied.\n */\nexport function autoFixSpec(spec: Spec): {\n  spec: Spec;\n  fixes: string[];\n} {\n  const fixes: string[] = [];\n  const fixedElements: Record<string, UIElement> = {};\n\n  for (const [key, element] of Object.entries(spec.elements)) {\n    const props = element.props as Record<string, unknown> | undefined;\n    let fixed = element;\n\n    if (props && \"visible\" in props && props.visible !== undefined) {\n      // Move visible from props to element level\n      const { visible, ...restProps } = fixed.props as Record<string, unknown>;\n      fixed = {\n        ...fixed,\n        props: restProps,\n        visible: visible as UIElement[\"visible\"],\n      };\n      fixes.push(`Moved \"visible\" from props to element level on \"${key}\".`);\n    }\n\n    let currentProps = fixed.props as Record<string, unknown> | undefined;\n    if (currentProps && \"on\" in currentProps && currentProps.on !== undefined) {\n      // Move on from props to element level\n      const { on, ...restProps } = currentProps;\n      fixed = {\n        ...fixed,\n        props: restProps,\n        on: on as UIElement[\"on\"],\n      };\n      fixes.push(`Moved \"on\" from props to element level on \"${key}\".`);\n    }\n\n    currentProps = fixed.props as Record<string, unknown> | undefined;\n    if (\n      currentProps &&\n      \"repeat\" in currentProps &&\n      currentProps.repeat !== undefined\n    ) {\n      // Move repeat from props to element level\n      const { repeat, ...restProps } = currentProps;\n      fixed = {\n        ...fixed,\n        props: restProps,\n        repeat: repeat as UIElement[\"repeat\"],\n      };\n      fixes.push(`Moved \"repeat\" from props to element level on \"${key}\".`);\n    }\n\n    currentProps = fixed.props as Record<string, unknown> | undefined;\n    if (\n      currentProps &&\n      \"watch\" in currentProps &&\n      currentProps.watch !== undefined\n    ) {\n      const { watch, ...restProps } = currentProps;\n      fixed = {\n        ...fixed,\n        props: restProps,\n        watch: watch as UIElement[\"watch\"],\n      };\n      fixes.push(`Moved \"watch\" from props to element level on \"${key}\".`);\n    }\n\n    fixedElements[key] = fixed;\n  }\n\n  return {\n    spec: { root: spec.root, elements: fixedElements, state: spec.state },\n    fixes,\n  };\n}\n\n/**\n * Format validation issues into a human-readable string suitable for\n * inclusion in a repair prompt sent back to the AI.\n */\nexport function formatSpecIssues(issues: SpecIssue[]): string {\n  const errors = issues.filter((i) => i.severity === \"error\");\n  if (errors.length === 0) return \"\";\n\n  const lines = [\"The generated UI spec has the following errors:\"];\n  for (const issue of errors) {\n    lines.push(`- ${issue.message}`);\n  }\n  return lines.join(\"\\n\");\n}\n"
  },
  {
    "path": "packages/core/src/state-store.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { createStateStore, flattenToPointers } from \"./state-store\";\n\ndescribe(\"createStateStore\", () => {\n  it(\"creates a store with initial state\", () => {\n    const store = createStateStore({ name: \"test\" });\n    expect(store.getSnapshot()).toEqual({ name: \"test\" });\n    expect(store.get(\"/name\")).toBe(\"test\");\n  });\n\n  it(\"set notifies subscribers\", () => {\n    const store = createStateStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).toHaveBeenCalledTimes(1);\n    expect(store.get(\"/x\")).toBe(1);\n  });\n\n  it(\"set skips notification when value is unchanged\", () => {\n    const store = createStateStore({ x: 1 });\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toEqual({ x: 1 });\n  });\n\n  it(\"update notifies subscribers once\", () => {\n    const store = createStateStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n    expect(store.get(\"/a\")).toBe(1);\n    expect(store.get(\"/b\")).toBe(2);\n  });\n\n  it(\"update skips notification when no values changed\", () => {\n    const store = createStateStore({ a: 1, b: 2 });\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).not.toHaveBeenCalled();\n  });\n\n  it(\"unsubscribe stops notifications\", () => {\n    const store = createStateStore({});\n    const listener = vi.fn();\n    const unsubscribe = store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n    expect(listener).toHaveBeenCalledTimes(1);\n\n    unsubscribe();\n    store.set(\"/x\", 2);\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"getSnapshot returns a new reference after mutation\", () => {\n    const store = createStateStore({ x: 1 });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/x\", 2);\n    const snap2 = store.getSnapshot();\n\n    expect(snap1).not.toBe(snap2);\n    expect(snap2.x).toBe(2);\n  });\n\n  it(\"getSnapshot returns same reference when set is a no-op\", () => {\n    const store = createStateStore({ x: 1 });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/x\", 1);\n    const snap2 = store.getSnapshot();\n\n    expect(snap1).toBe(snap2);\n  });\n\n  it(\"set on nested path does not mutate previous snapshot\", () => {\n    const store = createStateStore({ user: { name: \"Alice\", age: 30 } });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/user/name\", \"Bob\");\n    const snap2 = store.getSnapshot();\n\n    expect(snap1.user).toEqual({ name: \"Alice\", age: 30 });\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect(snap1.user).not.toBe(snap2.user);\n  });\n\n  it(\"update on nested paths does not mutate previous snapshot\", () => {\n    const store = createStateStore({\n      user: { name: \"Alice\" },\n      meta: { version: 1 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.update({ \"/user/name\": \"Bob\", \"/meta/version\": 2 });\n    const snap2 = store.getSnapshot();\n\n    expect((snap1.user as Record<string, unknown>).name).toBe(\"Alice\");\n    expect((snap1.meta as Record<string, unknown>).version).toBe(1);\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect((snap2.meta as Record<string, unknown>).version).toBe(2);\n  });\n\n  it(\"set preserves structural sharing for untouched branches\", () => {\n    const store = createStateStore({\n      a: { x: 1 },\n      b: { y: 2 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/a/x\", 99);\n    const snap2 = store.getSnapshot();\n\n    expect(snap2.b).toBe(snap1.b);\n    expect(snap2.a).not.toBe(snap1.a);\n  });\n\n  it(\"getServerSnapshot returns the same state as getSnapshot\", () => {\n    const store = createStateStore({ x: 1 });\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n\n    store.set(\"/x\", 2);\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n  });\n});\n\ndescribe(\"flattenToPointers\", () => {\n  it(\"flattens top-level keys\", () => {\n    expect(flattenToPointers({ a: 1, b: \"hello\" })).toEqual({\n      \"/a\": 1,\n      \"/b\": \"hello\",\n    });\n  });\n\n  it(\"flattens nested plain objects\", () => {\n    expect(flattenToPointers({ user: { name: \"Alice\", age: 30 } })).toEqual({\n      \"/user/name\": \"Alice\",\n      \"/user/age\": 30,\n    });\n  });\n\n  it(\"preserves arrays as leaf values\", () => {\n    expect(flattenToPointers({ items: [1, 2, 3] })).toEqual({\n      \"/items\": [1, 2, 3],\n    });\n  });\n\n  it(\"preserves null as a leaf value\", () => {\n    expect(flattenToPointers({ x: null })).toEqual({ \"/x\": null });\n  });\n\n  it(\"handles deeply nested objects\", () => {\n    expect(flattenToPointers({ a: { b: { c: 42 } } })).toEqual({\n      \"/a/b/c\": 42,\n    });\n  });\n\n  it(\"returns empty object for empty input\", () => {\n    expect(flattenToPointers({})).toEqual({});\n  });\n\n  it(\"handles mixed nesting\", () => {\n    expect(\n      flattenToPointers({\n        count: 1,\n        user: { name: \"Alice\" },\n        tags: [\"a\", \"b\"],\n      }),\n    ).toEqual({\n      \"/count\": 1,\n      \"/user/name\": \"Alice\",\n      \"/tags\": [\"a\", \"b\"],\n    });\n  });\n\n  it(\"stops recursion on circular references via seen set\", () => {\n    const obj: Record<string, unknown> = { name: \"root\" };\n    obj.self = obj;\n\n    const result = flattenToPointers(obj);\n\n    expect(result[\"/name\"]).toBe(\"root\");\n    expect(result[\"/self/name\"]).toBe(\"root\");\n    expect(result[\"/self/self\"]).toBe(obj);\n  });\n\n  it(\"caps recursion at depth limit\", () => {\n    let current: Record<string, unknown> = { leaf: true };\n    for (let i = 0; i < 25; i++) {\n      current = { nested: current };\n    }\n\n    const result = flattenToPointers(current);\n\n    const keys = Object.keys(result);\n    expect(keys.length).toBe(1);\n    const key = keys[0]!;\n    expect(key.split(\"/\").length).toBeLessThanOrEqual(22);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/state-store.ts",
    "content": "import {\n  getByPath,\n  parseJsonPointer,\n  type StateModel,\n  type StateStore,\n} from \"./types\";\n\n/**\n * Immutably set a value at a JSON Pointer path using structural sharing.\n * Only objects along the path are shallow-cloned; untouched branches keep\n * their original references.\n */\nexport function immutableSetByPath(\n  root: StateModel,\n  path: string,\n  value: unknown,\n): StateModel {\n  const segments = parseJsonPointer(path);\n  if (segments.length === 0) return root;\n\n  const result = { ...root };\n  let current: Record<string, unknown> = result;\n\n  for (let i = 0; i < segments.length - 1; i++) {\n    const seg = segments[i]!;\n    const child = current[seg];\n    if (Array.isArray(child)) {\n      current[seg] = [...child];\n    } else if (child !== null && typeof child === \"object\") {\n      current[seg] = { ...(child as Record<string, unknown>) };\n    } else {\n      const nextSeg = segments[i + 1];\n      current[seg] = nextSeg !== undefined && /^\\d+$/.test(nextSeg) ? [] : {};\n    }\n    current = current[seg] as Record<string, unknown>;\n  }\n\n  const lastSeg = segments[segments.length - 1]!;\n  if (Array.isArray(current)) {\n    if (lastSeg === \"-\") {\n      (current as unknown[]).push(value);\n    } else {\n      (current as unknown[])[parseInt(lastSeg, 10)] = value;\n    }\n  } else {\n    current[lastSeg] = value;\n  }\n\n  return result;\n}\n\n/**\n * Create a simple in-memory {@link StateStore}.\n *\n * This is the default store used by `StateProvider` when no external store is\n * provided. It mirrors the previous `useState`-based behaviour but is\n * framework-agnostic so it can also be used in tests or non-React contexts.\n */\nexport function createStateStore(initialState: StateModel = {}): StateStore {\n  let state: StateModel = { ...initialState };\n  const listeners = new Set<() => void>();\n\n  function notify() {\n    for (const listener of listeners) {\n      listener();\n    }\n  }\n\n  return {\n    get(path: string): unknown {\n      return getByPath(state, path);\n    },\n\n    set(path: string, value: unknown): void {\n      if (getByPath(state, path) === value) return;\n      state = immutableSetByPath(state, path, value);\n      notify();\n    },\n\n    update(updates: Record<string, unknown>): void {\n      let changed = false;\n      let next = state;\n      for (const [path, value] of Object.entries(updates)) {\n        if (getByPath(next, path) !== value) {\n          next = immutableSetByPath(next, path, value);\n          changed = true;\n        }\n      }\n      if (!changed) return;\n      state = next;\n      notify();\n    },\n\n    getSnapshot(): StateModel {\n      return state;\n    },\n\n    getServerSnapshot(): StateModel {\n      return state;\n    },\n\n    subscribe(listener: () => void): () => void {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n  };\n}\n\n/**\n * Configuration for {@link createStoreAdapter}. Adapter authors supply these\n * three callbacks; everything else (get, set, update, no-op detection,\n * getServerSnapshot) is handled by the returned {@link StateStore}.\n */\nexport interface StoreAdapterConfig {\n  /** Return the current state snapshot from the underlying store. */\n  getSnapshot: () => StateModel;\n  /** Write a new state snapshot to the underlying store. */\n  setSnapshot: (next: StateModel) => void;\n  /** Subscribe to changes in the underlying store. Return an unsubscribe fn. */\n  subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Build a full {@link StateStore} from a minimal adapter config.\n *\n * Handles `get`, `set` (with no-op detection), `update` (batched, with no-op\n * detection), `getSnapshot`, `getServerSnapshot`, and `subscribe` -- so each\n * adapter only needs to wire its snapshot source, write API, and subscribe\n * mechanism.\n */\nexport function createStoreAdapter(config: StoreAdapterConfig): StateStore {\n  return {\n    get(path: string): unknown {\n      return getByPath(config.getSnapshot(), path);\n    },\n\n    set(path: string, value: unknown): void {\n      const current = config.getSnapshot();\n      if (getByPath(current, path) === value) return;\n      config.setSnapshot(immutableSetByPath(current, path, value));\n    },\n\n    update(updates: Record<string, unknown>): void {\n      let next = config.getSnapshot();\n      let changed = false;\n      for (const [path, value] of Object.entries(updates)) {\n        if (getByPath(next, path) !== value) {\n          next = immutableSetByPath(next, path, value);\n          changed = true;\n        }\n      }\n      if (!changed) return;\n      config.setSnapshot(next);\n    },\n\n    getSnapshot: config.getSnapshot,\n\n    getServerSnapshot: config.getSnapshot,\n\n    subscribe: config.subscribe,\n  };\n}\n\nconst MAX_FLATTEN_DEPTH = 20;\n\n/**\n * Recursively flatten a plain object into a `Record<string, unknown>` keyed by\n * JSON Pointer paths. Only leaf values (non-plain-object) appear in the output.\n *\n * Includes circular reference protection and a depth cap to prevent stack\n * overflow on pathological inputs.\n *\n * ```ts\n * flattenToPointers({ user: { name: \"Alice\" }, count: 1 })\n * // => { \"/user/name\": \"Alice\", \"/count\": 1 }\n * ```\n */\nexport function flattenToPointers(\n  obj: Record<string, unknown>,\n  prefix = \"\",\n  _depth = 0,\n  _seen?: Set<object>,\n  _warned?: { current: boolean },\n): Record<string, unknown> {\n  const seen = _seen ?? new Set<object>();\n  const warned = _warned ?? { current: false };\n  const result: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(obj)) {\n    const pointer = `${prefix}/${key}`;\n    if (\n      _depth < MAX_FLATTEN_DEPTH &&\n      value !== null &&\n      typeof value === \"object\" &&\n      !Array.isArray(value) &&\n      Object.getPrototypeOf(value) === Object.prototype &&\n      !seen.has(value)\n    ) {\n      seen.add(value);\n      Object.assign(\n        result,\n        flattenToPointers(\n          value as Record<string, unknown>,\n          pointer,\n          _depth + 1,\n          seen,\n          warned,\n        ),\n      );\n    } else {\n      if (\n        process.env.NODE_ENV !== \"production\" &&\n        !warned.current &&\n        _depth >= MAX_FLATTEN_DEPTH &&\n        value !== null &&\n        typeof value === \"object\" &&\n        !Array.isArray(value) &&\n        Object.getPrototypeOf(value) === Object.prototype &&\n        !seen.has(value as object)\n      ) {\n        warned.current = true;\n        console.warn(\n          `flattenToPointers: depth limit (${MAX_FLATTEN_DEPTH}) reached. Nested state beyond this depth will be treated as a leaf value.`,\n        );\n      }\n      result[pointer] = value;\n    }\n  }\n  return result;\n}\n"
  },
  {
    "path": "packages/core/src/store-utils.ts",
    "content": "export {\n  immutableSetByPath,\n  flattenToPointers,\n  createStoreAdapter,\n} from \"./state-store\";\nexport type { StoreAdapterConfig } from \"./state-store\";\n"
  },
  {
    "path": "packages/core/src/types.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport {\n  resolveDynamicValue,\n  getByPath,\n  setByPath,\n  addByPath,\n  removeByPath,\n  applySpecStreamPatch,\n  applySpecPatch,\n  nestedToFlat,\n  compileSpecStream,\n  createSpecStreamCompiler,\n  createMixedStreamParser,\n  createJsonRenderTransform,\n  SPEC_DATA_PART_TYPE,\n} from \"./types\";\nimport type { Spec, SpecStreamLine, StreamChunk } from \"./types\";\n\ndescribe(\"getByPath\", () => {\n  it(\"gets nested values with JSON pointer paths\", () => {\n    const data = { user: { name: \"John\", scores: [10, 20, 30] } };\n\n    expect(getByPath(data, \"/user/name\")).toBe(\"John\");\n    expect(getByPath(data, \"/user/scores/0\")).toBe(10);\n    expect(getByPath(data, \"/user/scores/1\")).toBe(20);\n  });\n\n  it(\"returns root object for empty or root path\", () => {\n    const data = { value: 42 };\n\n    expect(getByPath(data, \"/\")).toBe(data);\n    expect(getByPath(data, \"\")).toBe(data);\n  });\n\n  it(\"returns undefined for missing paths\", () => {\n    const data = { user: { name: \"John\" } };\n\n    expect(getByPath(data, \"/user/missing\")).toBeUndefined();\n    expect(getByPath(data, \"/nonexistent/path\")).toBeUndefined();\n  });\n\n  it(\"handles paths without leading slash\", () => {\n    const data = { user: { name: \"John\" } };\n\n    expect(getByPath(data, \"user/name\")).toBe(\"John\");\n  });\n\n  it(\"returns undefined when traversing through non-object\", () => {\n    const data = { value: \"string\" };\n\n    expect(getByPath(data, \"/value/nested\")).toBeUndefined();\n  });\n\n  it(\"handles null values in path\", () => {\n    const data = { user: null };\n\n    expect(getByPath(data, \"/user/name\")).toBeUndefined();\n  });\n});\n\ndescribe(\"setByPath\", () => {\n  it(\"sets value at existing path\", () => {\n    const data: Record<string, unknown> = { user: { name: \"John\" } };\n\n    setByPath(data, \"/user/name\", \"Alice\");\n\n    expect((data.user as Record<string, unknown>).name).toBe(\"Alice\");\n  });\n\n  it(\"creates intermediate objects for deep paths\", () => {\n    const data: Record<string, unknown> = {};\n\n    setByPath(data, \"/a/b/c\", \"deep\");\n\n    expect(\n      ((data.a as Record<string, unknown>).b as Record<string, unknown>).c,\n    ).toBe(\"deep\");\n  });\n\n  it(\"handles paths without leading slash\", () => {\n    const data: Record<string, unknown> = {};\n\n    setByPath(data, \"user/name\", \"John\");\n\n    expect((data.user as Record<string, unknown>).name).toBe(\"John\");\n  });\n\n  it(\"overwrites existing values\", () => {\n    const data: Record<string, unknown> = { count: 5 };\n\n    setByPath(data, \"/count\", 10);\n\n    expect(data.count).toBe(10);\n  });\n\n  it(\"handles array-like paths\", () => {\n    const data: Record<string, unknown> = { items: {} };\n\n    setByPath(data, \"/items/0\", \"first\");\n\n    expect((data.items as Record<string, unknown>)[\"0\"]).toBe(\"first\");\n  });\n});\n\ndescribe(\"resolveDynamicValue\", () => {\n  it(\"resolves literal string values\", () => {\n    expect(resolveDynamicValue(\"hello\", {})).toBe(\"hello\");\n  });\n\n  it(\"resolves literal number values\", () => {\n    expect(resolveDynamicValue(42, {})).toBe(42);\n  });\n\n  it(\"resolves literal boolean values\", () => {\n    expect(resolveDynamicValue(true, {})).toBe(true);\n    expect(resolveDynamicValue(false, {})).toBe(false);\n  });\n\n  it(\"resolves $state references\", () => {\n    const data = { user: { name: \"Alice\" } };\n\n    expect(resolveDynamicValue({ $state: \"/user/name\" }, data)).toBe(\"Alice\");\n  });\n\n  it(\"returns undefined for missing $state references\", () => {\n    const data = { user: { name: \"Alice\" } };\n\n    expect(resolveDynamicValue({ $state: \"/missing\" }, data)).toBeUndefined();\n  });\n\n  it(\"returns undefined for null value\", () => {\n    expect(resolveDynamicValue(null as unknown as string, {})).toBeUndefined();\n  });\n\n  it(\"returns undefined for undefined value\", () => {\n    expect(\n      resolveDynamicValue(undefined as unknown as string, {}),\n    ).toBeUndefined();\n  });\n});\n\n// =============================================================================\n// JSON Pointer (RFC 6901) escaping\n// =============================================================================\n\ndescribe(\"JSON Pointer escaping (RFC 6901)\", () => {\n  it(\"getByPath unescapes ~1 to /\", () => {\n    const data = { \"a/b\": { c: 42 } };\n    expect(getByPath(data, \"/a~1b/c\")).toBe(42);\n  });\n\n  it(\"getByPath unescapes ~0 to ~\", () => {\n    const data = { \"a~b\": 99 };\n    expect(getByPath(data, \"/a~0b\")).toBe(99);\n  });\n\n  it(\"getByPath handles combined ~0 and ~1 escaping\", () => {\n    const data = { \"a~/b\": \"found\" };\n    expect(getByPath(data, \"/a~0~1b\")).toBe(\"found\");\n  });\n\n  it(\"setByPath unescapes ~1 to / in keys\", () => {\n    const data: Record<string, unknown> = {};\n    setByPath(data, \"/a~1b\", \"value\");\n    expect(data[\"a/b\"]).toBe(\"value\");\n  });\n\n  it(\"setByPath unescapes ~0 to ~ in keys\", () => {\n    const data: Record<string, unknown> = {};\n    setByPath(data, \"/a~0b\", \"value\");\n    expect(data[\"a~b\"]).toBe(\"value\");\n  });\n\n  it(\"getByPath reads array elements properly\", () => {\n    const data = { items: [10, 20, 30] };\n    expect(getByPath(data, \"/items/0\")).toBe(10);\n    expect(getByPath(data, \"/items/2\")).toBe(30);\n  });\n});\n\n// =============================================================================\n// addByPath (RFC 6902 \"add\" semantics)\n// =============================================================================\n\ndescribe(\"addByPath\", () => {\n  it(\"adds a property to an object\", () => {\n    const data: Record<string, unknown> = { a: 1 };\n    addByPath(data, \"/b\", 2);\n    expect(data.b).toBe(2);\n  });\n\n  it(\"replaces an existing object property (add semantics)\", () => {\n    const data: Record<string, unknown> = { a: 1 };\n    addByPath(data, \"/a\", 99);\n    expect(data.a).toBe(99);\n  });\n\n  it(\"inserts into an array at a specific index\", () => {\n    const data: Record<string, unknown> = { arr: [1, 2, 3] };\n    addByPath(data, \"/arr/1\", 99);\n    expect(data.arr).toEqual([1, 99, 2, 3]);\n  });\n\n  it(\"appends to an array with - (end-of-array)\", () => {\n    const data: Record<string, unknown> = { arr: [1, 2] };\n    addByPath(data, \"/arr/-\", 3);\n    expect(data.arr).toEqual([1, 2, 3]);\n  });\n\n  it(\"inserts at index 0 of an array\", () => {\n    const data: Record<string, unknown> = { arr: [\"b\", \"c\"] };\n    addByPath(data, \"/arr/0\", \"a\");\n    expect(data.arr).toEqual([\"a\", \"b\", \"c\"]);\n  });\n\n  it(\"creates intermediate objects\", () => {\n    const data: Record<string, unknown> = {};\n    addByPath(data, \"/x/y/z\", \"deep\");\n    expect(\n      ((data.x as Record<string, unknown>).y as Record<string, unknown>).z,\n    ).toBe(\"deep\");\n  });\n});\n\n// =============================================================================\n// removeByPath (RFC 6902 \"remove\" semantics)\n// =============================================================================\n\ndescribe(\"removeByPath\", () => {\n  it(\"deletes an object property\", () => {\n    const data: Record<string, unknown> = { a: 1, b: 2 };\n    removeByPath(data, \"/a\");\n    expect(data).toEqual({ b: 2 });\n    expect(\"a\" in data).toBe(false);\n  });\n\n  it(\"removes an element from an array by index\", () => {\n    const data: Record<string, unknown> = { arr: [1, 2, 3] };\n    removeByPath(data, \"/arr/1\");\n    expect(data.arr).toEqual([1, 3]);\n  });\n\n  it(\"removes nested properties\", () => {\n    const data: Record<string, unknown> = { user: { name: \"John\", age: 30 } };\n    removeByPath(data, \"/user/age\");\n    expect(data.user).toEqual({ name: \"John\" });\n  });\n\n  it(\"is a no-op for non-existent paths\", () => {\n    const data: Record<string, unknown> = { a: 1 };\n    removeByPath(data, \"/b/c/d\");\n    expect(data).toEqual({ a: 1 });\n  });\n});\n\n// =============================================================================\n// applySpecStreamPatch - RFC 6902 operations\n// =============================================================================\n\ndescribe(\"applySpecStreamPatch\", () => {\n  describe(\"add operation\", () => {\n    it(\"adds a new object property\", () => {\n      const obj: Record<string, unknown> = {};\n      applySpecStreamPatch(obj, { op: \"add\", path: \"/name\", value: \"Alice\" });\n      expect(obj.name).toBe(\"Alice\");\n    });\n\n    it(\"adds nested properties creating intermediates\", () => {\n      const obj: Record<string, unknown> = {};\n      applySpecStreamPatch(obj, {\n        op: \"add\",\n        path: \"/user/name\",\n        value: \"Bob\",\n      });\n      expect((obj.user as Record<string, unknown>).name).toBe(\"Bob\");\n    });\n\n    it(\"inserts into array at index\", () => {\n      const obj: Record<string, unknown> = { items: [1, 3] };\n      applySpecStreamPatch(obj, { op: \"add\", path: \"/items/1\", value: 2 });\n      expect(obj.items).toEqual([1, 2, 3]);\n    });\n\n    it(\"appends to array with -\", () => {\n      const obj: Record<string, unknown> = { items: [1, 2] };\n      applySpecStreamPatch(obj, { op: \"add\", path: \"/items/-\", value: 3 });\n      expect(obj.items).toEqual([1, 2, 3]);\n    });\n  });\n\n  describe(\"remove operation\", () => {\n    it(\"removes an object property\", () => {\n      const obj: Record<string, unknown> = { name: \"Alice\", age: 30 };\n      applySpecStreamPatch(obj, { op: \"remove\", path: \"/age\" });\n      expect(obj).toEqual({ name: \"Alice\" });\n      expect(\"age\" in obj).toBe(false);\n    });\n\n    it(\"removes from array by index\", () => {\n      const obj: Record<string, unknown> = { items: [\"a\", \"b\", \"c\"] };\n      applySpecStreamPatch(obj, { op: \"remove\", path: \"/items/1\" });\n      expect(obj.items).toEqual([\"a\", \"c\"]);\n    });\n  });\n\n  describe(\"replace operation\", () => {\n    it(\"replaces an existing value\", () => {\n      const obj: Record<string, unknown> = { name: \"Alice\" };\n      applySpecStreamPatch(obj, {\n        op: \"replace\",\n        path: \"/name\",\n        value: \"Bob\",\n      });\n      expect(obj.name).toBe(\"Bob\");\n    });\n\n    it(\"replaces nested values\", () => {\n      const obj: Record<string, unknown> = { user: { name: \"Alice\" } };\n      applySpecStreamPatch(obj, {\n        op: \"replace\",\n        path: \"/user/name\",\n        value: \"Bob\",\n      });\n      expect((obj.user as Record<string, unknown>).name).toBe(\"Bob\");\n    });\n  });\n\n  describe(\"move operation\", () => {\n    it(\"moves a value from one path to another\", () => {\n      const obj: Record<string, unknown> = { a: 1, b: 2 };\n      applySpecStreamPatch(obj, { op: \"move\", path: \"/c\", from: \"/a\" });\n      expect(obj).toEqual({ b: 2, c: 1 });\n      expect(\"a\" in obj).toBe(false);\n    });\n\n    it(\"moves nested values\", () => {\n      const obj: Record<string, unknown> = {\n        source: { val: 42 },\n        target: {},\n      };\n      applySpecStreamPatch(obj, {\n        op: \"move\",\n        path: \"/target/val\",\n        from: \"/source/val\",\n      });\n      expect((obj.target as Record<string, unknown>).val).toBe(42);\n      expect(\"val\" in (obj.source as Record<string, unknown>)).toBe(false);\n    });\n\n    it(\"is a no-op if from is missing\", () => {\n      const obj: Record<string, unknown> = { a: 1 };\n      applySpecStreamPatch(obj, { op: \"move\", path: \"/b\" });\n      expect(obj).toEqual({ a: 1 });\n    });\n  });\n\n  describe(\"copy operation\", () => {\n    it(\"copies a value from one path to another\", () => {\n      const obj: Record<string, unknown> = { a: 1 };\n      applySpecStreamPatch(obj, { op: \"copy\", path: \"/b\", from: \"/a\" });\n      expect(obj).toEqual({ a: 1, b: 1 });\n    });\n\n    it(\"copies complex values\", () => {\n      const obj: Record<string, unknown> = {\n        source: { nested: { value: 42 } },\n      };\n      applySpecStreamPatch(obj, {\n        op: \"copy\",\n        path: \"/dest\",\n        from: \"/source/nested\",\n      });\n      expect(obj.dest).toEqual({ value: 42 });\n      // Source should still exist\n      expect((obj.source as Record<string, unknown>).nested).toEqual({\n        value: 42,\n      });\n    });\n\n    it(\"is a no-op if from is missing\", () => {\n      const obj: Record<string, unknown> = { a: 1 };\n      applySpecStreamPatch(obj, { op: \"copy\", path: \"/b\" });\n      expect(obj).toEqual({ a: 1 });\n    });\n  });\n\n  describe(\"test operation\", () => {\n    it(\"succeeds when values match\", () => {\n      const obj: Record<string, unknown> = { name: \"Alice\" };\n      expect(() =>\n        applySpecStreamPatch(obj, {\n          op: \"test\",\n          path: \"/name\",\n          value: \"Alice\",\n        }),\n      ).not.toThrow();\n    });\n\n    it(\"succeeds for matching objects\", () => {\n      const obj: Record<string, unknown> = { user: { name: \"Alice\", age: 30 } };\n      expect(() =>\n        applySpecStreamPatch(obj, {\n          op: \"test\",\n          path: \"/user\",\n          value: { name: \"Alice\", age: 30 },\n        }),\n      ).not.toThrow();\n    });\n\n    it(\"succeeds for matching arrays\", () => {\n      const obj: Record<string, unknown> = { items: [1, 2, 3] };\n      expect(() =>\n        applySpecStreamPatch(obj, {\n          op: \"test\",\n          path: \"/items\",\n          value: [1, 2, 3],\n        }),\n      ).not.toThrow();\n    });\n\n    it(\"throws when values do not match\", () => {\n      const obj: Record<string, unknown> = { name: \"Alice\" };\n      expect(() =>\n        applySpecStreamPatch(obj, {\n          op: \"test\",\n          path: \"/name\",\n          value: \"Bob\",\n        }),\n      ).toThrow('Test operation failed: value at \"/name\" does not match');\n    });\n\n    it(\"throws when path does not exist\", () => {\n      const obj: Record<string, unknown> = {};\n      expect(() =>\n        applySpecStreamPatch(obj, {\n          op: \"test\",\n          path: \"/missing\",\n          value: \"anything\",\n        }),\n      ).toThrow();\n    });\n  });\n});\n\n// =============================================================================\n// compileSpecStream\n// =============================================================================\n\ndescribe(\"compileSpecStream\", () => {\n  it(\"compiles a series of add patches\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n{\"op\":\"add\",\"path\":\"/age\",\"value\":30}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ name: \"Alice\", age: 30 });\n  });\n\n  it(\"handles remove operations\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/a\",\"value\":1}\n{\"op\":\"add\",\"path\":\"/b\",\"value\":2}\n{\"op\":\"remove\",\"path\":\"/a\"}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ b: 2 });\n    expect(\"a\" in result).toBe(false);\n  });\n\n  it(\"handles replace operations\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n{\"op\":\"replace\",\"path\":\"/name\",\"value\":\"Bob\"}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ name: \"Bob\" });\n  });\n\n  it(\"handles move operations\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/old\",\"value\":\"data\"}\n{\"op\":\"move\",\"path\":\"/new\",\"from\":\"/old\"}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ new: \"data\" });\n  });\n\n  it(\"handles copy operations\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/original\",\"value\":\"data\"}\n{\"op\":\"copy\",\"path\":\"/duplicate\",\"from\":\"/original\"}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ original: \"data\", duplicate: \"data\" });\n  });\n\n  it(\"skips empty lines\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/a\",\"value\":1}\n\n{\"op\":\"add\",\"path\":\"/b\",\"value\":2}`;\n    const result = compileSpecStream(stream);\n    expect(result).toEqual({ a: 1, b: 2 });\n  });\n\n  it(\"uses initial value\", () => {\n    const stream = `{\"op\":\"add\",\"path\":\"/b\",\"value\":2}`;\n    const result = compileSpecStream(stream, { a: 1 });\n    expect(result).toEqual({ a: 1, b: 2 });\n  });\n});\n\n// =============================================================================\n// createSpecStreamCompiler\n// =============================================================================\n\ndescribe(\"createSpecStreamCompiler\", () => {\n  it(\"processes chunks incrementally\", () => {\n    const compiler = createSpecStreamCompiler();\n    const { result, newPatches } = compiler.push(\n      '{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\\n',\n    );\n    expect(newPatches).toHaveLength(1);\n    expect(result).toEqual({ name: \"Alice\" });\n  });\n\n  it(\"handles partial lines across chunks\", () => {\n    const compiler = createSpecStreamCompiler();\n\n    // First chunk is incomplete\n    const r1 = compiler.push('{\"op\":\"add\",\"path\":\"/na');\n    expect(r1.newPatches).toHaveLength(0);\n\n    // Complete the line\n    const r2 = compiler.push('me\",\"value\":\"Alice\"}\\n');\n    expect(r2.newPatches).toHaveLength(1);\n    expect(r2.result).toEqual({ name: \"Alice\" });\n  });\n\n  it(\"processes remaining buffer on getResult\", () => {\n    const compiler = createSpecStreamCompiler();\n    compiler.push('{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}');\n    // No newline, so not processed yet\n    const result = compiler.getResult();\n    expect(result).toEqual({ name: \"Alice\" });\n  });\n\n  it(\"resets to clean state\", () => {\n    const compiler = createSpecStreamCompiler();\n    compiler.push('{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\\n');\n    compiler.reset();\n    const result = compiler.getResult();\n    expect(result).toEqual({});\n  });\n\n  it(\"tracks all applied patches\", () => {\n    const compiler = createSpecStreamCompiler();\n    compiler.push('{\"op\":\"add\",\"path\":\"/a\",\"value\":1}\\n');\n    compiler.push('{\"op\":\"add\",\"path\":\"/b\",\"value\":2}\\n');\n    const patches = compiler.getPatches();\n    expect(patches).toHaveLength(2);\n    expect(patches[0]!.op).toBe(\"add\");\n    expect(patches[1]!.op).toBe(\"add\");\n  });\n\n  it(\"handles all RFC 6902 operations\", () => {\n    const compiler = createSpecStreamCompiler();\n    compiler.push('{\"op\":\"add\",\"path\":\"/x\",\"value\":1}\\n');\n    compiler.push('{\"op\":\"add\",\"path\":\"/y\",\"value\":2}\\n');\n    compiler.push('{\"op\":\"move\",\"path\":\"/z\",\"from\":\"/x\"}\\n');\n    compiler.push('{\"op\":\"copy\",\"path\":\"/w\",\"from\":\"/y\"}\\n');\n    compiler.push('{\"op\":\"remove\",\"path\":\"/y\"}\\n');\n    const result = compiler.getResult();\n    expect(result).toEqual({ z: 1, w: 2 });\n  });\n});\n\n// =============================================================================\n// applySpecPatch\n// =============================================================================\n\ndescribe(\"applySpecPatch\", () => {\n  it(\"sets the root element\", () => {\n    const spec: Spec = { root: \"\", elements: {} };\n    applySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n    expect(spec.root).toBe(\"main\");\n  });\n\n  it(\"adds an element to the elements map\", () => {\n    const spec: Spec = { root: \"main\", elements: {} };\n    applySpecPatch(spec, {\n      op: \"add\",\n      path: \"/elements/main\",\n      value: { type: \"Card\", props: { title: \"Hello\" }, children: [] },\n    });\n    expect(spec.elements.main).toEqual({\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [],\n    });\n  });\n\n  it(\"replaces an existing element\", () => {\n    const spec: Spec = {\n      root: \"main\",\n      elements: {\n        main: { type: \"Card\", props: { title: \"Old\" }, children: [] },\n      },\n    };\n    applySpecPatch(spec, {\n      op: \"replace\",\n      path: \"/elements/main/props/title\",\n      value: \"New\",\n    });\n    expect(spec.elements.main!.props.title).toBe(\"New\");\n  });\n\n  it(\"removes an element\", () => {\n    const spec: Spec = {\n      root: \"main\",\n      elements: {\n        main: { type: \"Card\", props: {}, children: [] },\n        child: { type: \"Text\", props: {}, children: [] },\n      },\n    };\n    applySpecPatch(spec, { op: \"remove\", path: \"/elements/child\" });\n    expect(spec.elements.child).toBeUndefined();\n    expect(spec.elements.main).toBeDefined();\n  });\n\n  it(\"adds state data\", () => {\n    const spec: Spec = { root: \"main\", elements: {} };\n    applySpecPatch(spec, {\n      op: \"add\",\n      path: \"/state\",\n      value: { count: 0 },\n    });\n    expect(spec.state).toEqual({ count: 0 });\n  });\n\n  it(\"returns the mutated spec\", () => {\n    const spec: Spec = { root: \"\", elements: {} };\n    const result = applySpecPatch(spec, {\n      op: \"add\",\n      path: \"/root\",\n      value: \"main\",\n    });\n    expect(result).toBe(spec);\n  });\n});\n\n// =============================================================================\n// createMixedStreamParser\n// =============================================================================\n\ndescribe(\"createMixedStreamParser\", () => {\n  it(\"classifies JSONL lines as patches\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push('{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}\\n');\n    expect(patches).toHaveLength(1);\n    expect(patches[0]!.op).toBe(\"add\");\n    expect(patches[0]!.path).toBe(\"/root\");\n    expect(texts).toHaveLength(0);\n  });\n\n  it(\"classifies non-JSONL lines as text\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\"Hello, here is your UI:\\n\");\n    expect(texts).toHaveLength(1);\n    expect(texts[0]).toBe(\"Hello, here is your UI:\");\n    expect(patches).toHaveLength(0);\n  });\n\n  it(\"handles mixed text and JSONL\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\n      'Here is the dashboard:\\n{\"op\":\"add\",\"path\":\"/root\",\"value\":\"dash\"}\\n',\n    );\n    expect(texts).toHaveLength(1);\n    expect(texts[0]).toBe(\"Here is the dashboard:\");\n    expect(patches).toHaveLength(1);\n    expect(patches[0]!.path).toBe(\"/root\");\n  });\n\n  it(\"buffers partial lines across chunks\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push('{\"op\":\"add\",\"path\":\"/ro');\n    expect(patches).toHaveLength(0);\n\n    parser.push('ot\",\"value\":\"main\"}\\n');\n    expect(patches).toHaveLength(1);\n    expect(patches[0]!.path).toBe(\"/root\");\n  });\n\n  it(\"flushes remaining buffer on flush()\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    // No trailing newline — stuck in buffer\n    parser.push('{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}');\n    expect(patches).toHaveLength(0);\n\n    parser.flush();\n    expect(patches).toHaveLength(1);\n  });\n\n  it(\"flushes text buffer on flush()\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\"Some trailing text\");\n    expect(texts).toHaveLength(0);\n\n    parser.flush();\n    expect(texts).toHaveLength(1);\n    expect(texts[0]).toBe(\"Some trailing text\");\n  });\n\n  it(\"skips empty lines\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\"\\n\\n\\n\");\n    expect(patches).toHaveLength(0);\n    expect(texts).toHaveLength(0);\n  });\n});\n\n// =============================================================================\n// nestedToFlat\n// =============================================================================\n\ndescribe(\"nestedToFlat\", () => {\n  it(\"converts a single node with no children\", () => {\n    const spec = nestedToFlat({\n      type: \"Text\",\n      props: { content: \"Hello\" },\n    });\n    expect(spec.root).toBe(\"el-0\");\n    expect(spec.elements[\"el-0\"]).toEqual({\n      type: \"Text\",\n      props: { content: \"Hello\" },\n      children: [],\n    });\n  });\n\n  it(\"converts a tree with children\", () => {\n    const spec = nestedToFlat({\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [\n        { type: \"Text\", props: { content: \"World\" }, children: [] },\n        { type: \"Button\", props: { label: \"Click\" } },\n      ],\n    });\n\n    expect(spec.root).toBe(\"el-0\");\n    expect(Object.keys(spec.elements)).toHaveLength(3);\n    expect(spec.elements[\"el-0\"]!.type).toBe(\"Card\");\n    expect(spec.elements[\"el-0\"]!.children).toEqual([\"el-1\", \"el-2\"]);\n    expect(spec.elements[\"el-1\"]!.type).toBe(\"Text\");\n    expect(spec.elements[\"el-1\"]!.children).toEqual([]);\n    expect(spec.elements[\"el-2\"]!.type).toBe(\"Button\");\n    expect(spec.elements[\"el-2\"]!.children).toEqual([]);\n  });\n\n  it(\"hoists state from root node\", () => {\n    const spec = nestedToFlat({\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [],\n      state: { count: 0, items: [] },\n    });\n\n    expect(spec.state).toEqual({ count: 0, items: [] });\n    // state should not appear as a field on the element\n    expect(\n      (spec.elements[\"el-0\"] as Record<string, unknown>).state,\n    ).toBeUndefined();\n  });\n\n  it(\"preserves extra fields like visible and on\", () => {\n    const spec = nestedToFlat({\n      type: \"Panel\",\n      props: {},\n      visible: { $state: \"/showPanel\" },\n      on: {\n        press: { action: \"setState\", params: { statePath: \"/x\", value: 1 } },\n      },\n      children: [],\n    });\n\n    const el = spec.elements[\"el-0\"] as Record<string, unknown>;\n    expect(el.visible).toEqual({ $state: \"/showPanel\" });\n    expect(el.on).toEqual({\n      press: { action: \"setState\", params: { statePath: \"/x\", value: 1 } },\n    });\n  });\n\n  it(\"handles deeply nested trees\", () => {\n    const spec = nestedToFlat({\n      type: \"A\",\n      props: {},\n      children: [\n        {\n          type: \"B\",\n          props: {},\n          children: [\n            {\n              type: \"C\",\n              props: {},\n              children: [{ type: \"D\", props: {} }],\n            },\n          ],\n        },\n      ],\n    });\n\n    expect(Object.keys(spec.elements)).toHaveLength(4);\n    expect(spec.elements[\"el-0\"]!.children).toEqual([\"el-1\"]);\n    expect(spec.elements[\"el-1\"]!.children).toEqual([\"el-2\"]);\n    expect(spec.elements[\"el-2\"]!.children).toEqual([\"el-3\"]);\n    expect(spec.elements[\"el-3\"]!.children).toEqual([]);\n  });\n\n  it(\"returns empty elements for empty children array\", () => {\n    const spec = nestedToFlat({\n      type: \"Empty\",\n      props: {},\n      children: [],\n    });\n\n    expect(Object.keys(spec.elements)).toHaveLength(1);\n    expect(spec.elements[\"el-0\"]!.children).toEqual([]);\n  });\n\n  it(\"does not hoist state from non-root nodes\", () => {\n    const spec = nestedToFlat({\n      type: \"Root\",\n      props: {},\n      children: [{ type: \"Child\", props: {}, state: { shouldNotHoist: true } }],\n    });\n\n    // Only root state hoists to spec.state\n    expect(spec.state).toBeUndefined();\n    // The child's state is not a standard field, so it should not leak\n    expect(\n      (spec.elements[\"el-1\"] as Record<string, unknown>).state,\n    ).toBeUndefined();\n  });\n});\n\n// =============================================================================\n// createJsonRenderTransform\n// =============================================================================\n\ndescribe(\"createJsonRenderTransform\", () => {\n  /** Helper: push text-delta chunks through the transform and collect output */\n  async function transformText(text: string): Promise<StreamChunk[]> {\n    const transform = createJsonRenderTransform();\n    const writer = transform.writable.getWriter();\n    const reader = transform.readable.getReader();\n\n    const chunks: StreamChunk[] = [];\n\n    // Read and write concurrently to avoid backpressure deadlock\n    const readAll = (async () => {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        chunks.push(value);\n      }\n    })();\n\n    await writer.write({ type: \"text-start\", id: \"t1\" });\n    await writer.write({ type: \"text-delta\", id: \"t1\", delta: text });\n    await writer.write({ type: \"text-end\", id: \"t1\" });\n    await writer.close();\n\n    await readAll;\n    return chunks;\n  }\n\n  it(\"passes prose text through as text-delta\", async () => {\n    const chunks = await transformText(\"Hello world\\n\");\n    const textChunks = chunks.filter((c) => c.type === \"text-delta\");\n    const text = textChunks.map((c) => (c as { delta: string }).delta).join(\"\");\n    expect(text).toContain(\"Hello world\");\n  });\n\n  it(\"classifies valid JSONL patches as data-spec (heuristic mode)\", async () => {\n    const patch = '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}\\n';\n    const chunks = await transformText(patch);\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(1);\n    expect((specChunks[0] as { data: { type: string } }).data.type).toBe(\n      \"patch\",\n    );\n  });\n\n  it(\"lines starting with { that are NOT patches are flushed as text\", async () => {\n    const line = '{\"not\":\"a patch\"}\\n';\n    const chunks = await transformText(line);\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    const textChunks = chunks.filter((c) => c.type === \"text-delta\");\n    expect(specChunks.length).toBe(0);\n    const text = textChunks.map((c) => (c as { delta: string }).delta).join(\"\");\n    expect(text).toContain('{\"not\":\"a patch\"}');\n  });\n\n  it(\"parses content inside ```spec fence as patches\", async () => {\n    const input = [\n      \"Here is some UI:\\n\",\n      \"```spec\\n\",\n      '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}\\n',\n      '{\"op\":\"add\",\"path\":\"/elements/main\",\"value\":{\"type\":\"Card\",\"props\":{},\"children\":[]}}\\n',\n      \"```\\n\",\n      \"Done!\\n\",\n    ].join(\"\");\n\n    const chunks = await transformText(input);\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(2);\n\n    // Prose before and after should come through\n    const textChunks = chunks.filter((c) => c.type === \"text-delta\");\n    const text = textChunks.map((c) => (c as { delta: string }).delta).join(\"\");\n    expect(text).toContain(\"Here is some UI:\");\n    expect(text).toContain(\"Done!\");\n    // Fence delimiters should NOT appear in text\n    expect(text).not.toContain(\"```spec\");\n  });\n\n  it(\"handles mixed text + heuristic patches in single stream\", async () => {\n    const input = [\n      \"Some text\\n\",\n      '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"r\"}\\n',\n      \"More text\\n\",\n    ].join(\"\");\n\n    const chunks = await transformText(input);\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(1);\n\n    const textChunks = chunks.filter((c) => c.type === \"text-delta\");\n    const text = textChunks.map((c) => (c as { delta: string }).delta).join(\"\");\n    expect(text).toContain(\"Some text\");\n    expect(text).toContain(\"More text\");\n  });\n\n  it(\"non-text chunks pass through unchanged\", async () => {\n    const transform = createJsonRenderTransform();\n    const writer = transform.writable.getWriter();\n    const reader = transform.readable.getReader();\n\n    const toolChunk = {\n      type: \"tool-call\",\n      toolCallId: \"abc\",\n      toolName: \"test\",\n    };\n\n    const readPromise = reader.read();\n    await writer.write(toolChunk as StreamChunk);\n    await writer.close();\n\n    const { value } = await readPromise;\n    expect(value).toEqual(toolChunk);\n  });\n\n  it(\"flush behavior at end of stream\", async () => {\n    const transform = createJsonRenderTransform();\n    const writer = transform.writable.getWriter();\n    const reader = transform.readable.getReader();\n\n    const chunks: StreamChunk[] = [];\n    const readAll = (async () => {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        chunks.push(value);\n      }\n    })();\n\n    // Write a text delta with no trailing newline\n    await writer.write({ type: \"text-start\", id: \"t1\" });\n    await writer.write({\n      type: \"text-delta\",\n      id: \"t1\",\n      delta: '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}',\n    });\n    await writer.write({ type: \"text-end\", id: \"t1\" });\n    await writer.close();\n\n    await readAll;\n\n    // The buffered patch should be flushed on text-end\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(1);\n  });\n\n  // ===========================================================================\n  // Text block splitting around spec data\n  // ===========================================================================\n\n  it(\"splits text blocks around spec data (text-start/text-end pairs)\", async () => {\n    const input = [\n      \"Some text\\n\",\n      '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"r\"}\\n',\n      \"More text\\n\",\n    ].join(\"\");\n\n    const chunks = await transformText(input);\n\n    const textStarts = chunks.filter((c) => c.type === \"text-start\");\n    const textEnds = chunks.filter((c) => c.type === \"text-end\");\n\n    // There should be two text blocks: one before the patch and one after\n    expect(textStarts.length).toBe(2);\n    expect(textEnds.length).toBe(2);\n\n    // Spec data should appear between the two text blocks\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(1);\n\n    // Find the indices of the first text-end and the spec chunk\n    const firstTextEndIdx = chunks.findIndex((c) => c.type === \"text-end\");\n    const specIdx = chunks.findIndex((c) => c.type === SPEC_DATA_PART_TYPE);\n    const secondTextStartIdx = chunks.findIndex(\n      (c, i) => i > specIdx && c.type === \"text-start\",\n    );\n    expect(firstTextEndIdx).toBeLessThan(specIdx);\n    expect(specIdx).toBeLessThan(secondTextStartIdx);\n  });\n\n  it(\"flush closes an open text block when stream ends without text-end\", async () => {\n    const transform = createJsonRenderTransform();\n    const writer = transform.writable.getWriter();\n    const reader = transform.readable.getReader();\n\n    const chunks: StreamChunk[] = [];\n    const readAll = (async () => {\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        chunks.push(value);\n      }\n    })();\n\n    // Write text-start + text-delta, then close WITHOUT text-end\n    await writer.write({ type: \"text-start\", id: \"t1\" });\n    await writer.write({\n      type: \"text-delta\",\n      id: \"t1\",\n      delta: \"Hello world\\n\",\n    });\n    await writer.close();\n\n    await readAll;\n\n    // The transform's flush should have emitted a text-end to close the block\n    const textEnds = chunks.filter((c) => c.type === \"text-end\");\n    expect(textEnds.length).toBe(1);\n\n    // Text content should still be present\n    const textChunks = chunks.filter((c) => c.type === \"text-delta\");\n    const text = textChunks.map((c) => (c as { delta: string }).delta).join(\"\");\n    expect(text).toContain(\"Hello world\");\n  });\n\n  it(\"consecutive patches do not produce empty text blocks\", async () => {\n    const input = [\n      '{\"op\":\"add\",\"path\":\"/root\",\"value\":\"r\"}\\n',\n      '{\"op\":\"add\",\"path\":\"/elements/r\",\"value\":{\"type\":\"Card\",\"props\":{},\"children\":[]}}\\n',\n    ].join(\"\");\n\n    const chunks = await transformText(input);\n\n    const specChunks = chunks.filter((c) => c.type === SPEC_DATA_PART_TYPE);\n    expect(specChunks.length).toBe(2);\n\n    // There should be no text-start/text-end pairs between the two spec chunks\n    // (the initial text-start from the upstream is forwarded, but no new empty ones)\n    const textDeltas = chunks.filter((c) => c.type === \"text-delta\");\n    const textContent = textDeltas\n      .map((c) => (c as { delta: string }).delta)\n      .join(\"\")\n      .trim();\n    // No meaningful text content between the patches\n    expect(textContent).toBe(\"\");\n\n    // Count text blocks: there should be at most 1 (the initial upstream one),\n    // not extra empty ones inserted between patches\n    const textStarts = chunks.filter((c) => c.type === \"text-start\");\n    const textEnds = chunks.filter((c) => c.type === \"text-end\");\n    expect(textStarts.length).toBeLessThanOrEqual(1);\n    expect(textEnds.length).toBeLessThanOrEqual(1);\n  });\n});\n\n// =============================================================================\n// createMixedStreamParser - fence mode\n// =============================================================================\n\ndescribe(\"createMixedStreamParser - fence mode\", () => {\n  it(\"parses patches inside ```spec fence\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\"Hello\\n\");\n    parser.push(\"```spec\\n\");\n    parser.push('{\"op\":\"add\",\"path\":\"/root\",\"value\":\"main\"}\\n');\n    parser.push(\"```\\n\");\n    parser.push(\"Goodbye\\n\");\n    parser.flush();\n\n    expect(patches.length).toBe(1);\n    expect(patches[0].op).toBe(\"add\");\n    expect(texts).toContain(\"Hello\");\n    expect(texts).toContain(\"Goodbye\");\n  });\n\n  it(\"fence delimiters are not emitted as text or patches\", () => {\n    const patches: SpecStreamLine[] = [];\n    const texts: string[] = [];\n    const parser = createMixedStreamParser({\n      onPatch: (p) => patches.push(p),\n      onText: (t) => texts.push(t),\n    });\n\n    parser.push(\"```spec\\n\");\n    parser.push('{\"op\":\"add\",\"path\":\"/root\",\"value\":\"r\"}\\n');\n    parser.push(\"```\\n\");\n    parser.flush();\n\n    expect(patches.length).toBe(1);\n    expect(texts.length).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/types.ts",
    "content": "import { z } from \"zod\";\nimport type { ActionBinding } from \"./actions\";\n\n/**\n * Dynamic value - can be a literal or a `{ $state }` reference to the state model.\n *\n * Used in action params and validation args where values can either be\n * hardcoded or resolved from state at runtime.\n */\nexport type DynamicValue<T = unknown> = T | { $state: string };\n\n/**\n * Dynamic string value\n */\nexport type DynamicString = DynamicValue<string>;\n\n/**\n * Dynamic number value\n */\nexport type DynamicNumber = DynamicValue<number>;\n\n/**\n * Dynamic boolean value\n */\nexport type DynamicBoolean = DynamicValue<boolean>;\n\n/**\n * Zod schema for dynamic values\n */\nexport const DynamicValueSchema = z.union([\n  z.string(),\n  z.number(),\n  z.boolean(),\n  z.null(),\n  z.object({ $state: z.string() }),\n]);\n\nexport const DynamicStringSchema = z.union([\n  z.string(),\n  z.object({ $state: z.string() }),\n]);\n\nexport const DynamicNumberSchema = z.union([\n  z.number(),\n  z.object({ $state: z.string() }),\n]);\n\nexport const DynamicBooleanSchema = z.union([\n  z.boolean(),\n  z.object({ $state: z.string() }),\n]);\n\n/**\n * Base UI element structure for v2\n */\nexport interface UIElement<\n  T extends string = string,\n  P = Record<string, unknown>,\n> {\n  /** Component type from the catalog */\n  type: T;\n  /** Component props */\n  props: P;\n  /** Child element keys (flat structure) */\n  children?: string[];\n  /** Visibility condition */\n  visible?: VisibilityCondition;\n  /** Event bindings — maps event names to action bindings */\n  on?: Record<string, ActionBinding | ActionBinding[]>;\n  /** Repeat children once per item in a state array */\n  repeat?: { statePath: string; key?: string };\n  /**\n   * State watchers — maps JSON Pointer state paths to action bindings.\n   * When the value at a watched path changes, the bound actions fire.\n   * Useful for cascading dependencies (e.g. country → city option loading).\n   */\n  watch?: Record<string, ActionBinding | ActionBinding[]>;\n}\n\n/**\n * Element with key and parentKey for use with flatToTree.\n * When elements are in an array (not a keyed map), key and parentKey\n * are needed to establish identity and parent-child relationships.\n */\nexport interface FlatElement<\n  T extends string = string,\n  P = Record<string, unknown>,\n> extends UIElement<T, P> {\n  /** Unique key identifying this element */\n  key: string;\n  /** Parent element key (null for root) */\n  parentKey?: string | null;\n}\n\n/**\n * Shared comparison operators for visibility conditions.\n *\n * Use at most ONE comparison operator per condition. If multiple are\n * provided, only the first matching one is evaluated (precedence:\n * eq > neq > gt > gte > lt > lte). With no operator, truthiness is checked.\n *\n * `not` inverts the final result of whichever operator (or truthiness\n * check) is used.\n */\ntype ComparisonOperators = {\n  eq?: unknown;\n  neq?: unknown;\n  gt?: number | { $state: string };\n  gte?: number | { $state: string };\n  lt?: number | { $state: string };\n  lte?: number | { $state: string };\n  not?: true;\n};\n\n/**\n * A single state-based condition.\n * Resolves `$state` to a value from the state model, then applies the operator.\n * Without an operator, checks truthiness.\n *\n * When `not` is `true`, the result of the entire condition is inverted.\n * For example `{ $state: \"/count\", gt: 5, not: true }` means \"NOT greater than 5\".\n */\nexport type StateCondition = { $state: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$item` to a field on the current repeat item.\n * Only meaningful inside a `repeat` scope.\n *\n * Use `\"\"` to reference the whole item, or `\"field\"` for a specific field.\n */\nexport type ItemCondition = { $item: string } & ComparisonOperators;\n\n/**\n * A condition that resolves `$index` to the current repeat array index.\n * Only meaningful inside a `repeat` scope.\n */\nexport type IndexCondition = { $index: true } & ComparisonOperators;\n\n/** A single visibility condition (state, item, or index). */\nexport type SingleCondition = StateCondition | ItemCondition | IndexCondition;\n\n/**\n * AND wrapper — all child conditions must be true.\n * This is the explicit form of the implicit array AND (`SingleCondition[]`).\n * Unlike the implicit form, `$and` supports nested `$or` and `$and` conditions.\n */\nexport type AndCondition = { $and: VisibilityCondition[] };\n\n/**\n * OR wrapper — at least one child condition must be true.\n */\nexport type OrCondition = { $or: VisibilityCondition[] };\n\n/**\n * Visibility condition types.\n * - `boolean` — always/never\n * - `SingleCondition` — single condition (`$state`, `$item`, or `$index`)\n * - `SingleCondition[]` — implicit AND (all must be true)\n * - `AndCondition` — `{ $and: [...] }`, explicit AND (all must be true)\n * - `OrCondition` — `{ $or: [...] }`, at least one must be true\n */\nexport type VisibilityCondition =\n  | boolean\n  | SingleCondition\n  | SingleCondition[]\n  | AndCondition\n  | OrCondition;\n\n/**\n * Flat UI tree structure (optimized for LLM generation)\n */\nexport interface Spec {\n  /** Root element key */\n  root: string;\n  /** Flat map of elements by key */\n  elements: Record<string, UIElement>;\n  /** Optional initial state to seed the state model.\n   *  Components using statePath will read from / write to this state. */\n  state?: Record<string, unknown>;\n}\n\n/**\n * State model type\n */\nexport type StateModel = Record<string, unknown>;\n\n/**\n * An abstract store that owns state and notifies subscribers on change.\n *\n * Consumers can supply their own implementation (backed by Redux, Zustand,\n * XState, etc.) or use the built-in {@link createStateStore} for a simple\n * in-memory store.\n */\nexport interface StateStore {\n  /** Read a value by JSON Pointer path. */\n  get: (path: string) => unknown;\n  /**\n   * Write a value by JSON Pointer path and notify subscribers.\n   * Equality is checked by reference (`===`), not deep comparison.\n   * Callers must pass a new object/array reference for changes to be detected.\n   */\n  set: (path: string, value: unknown) => void;\n  /**\n   * Write multiple values at once and notify subscribers (single notification).\n   * Each value is compared by reference (`===`); only paths whose value\n   * actually changed are applied.\n   */\n  update: (updates: Record<string, unknown>) => void;\n  /** Return the full state object (used by `useSyncExternalStore`). */\n  getSnapshot: () => StateModel;\n  /** Optional server snapshot for SSR (passed to `useSyncExternalStore`). Falls back to `getSnapshot` when omitted. */\n  getServerSnapshot?: () => StateModel;\n  /** Register a listener that is called on every state change. Returns an unsubscribe function. */\n  subscribe: (listener: () => void) => () => void;\n}\n\n/**\n * Component schema definition using Zod\n */\nexport type ComponentSchema = z.ZodType<Record<string, unknown>>;\n\n/**\n * Validation mode for catalog validation\n */\nexport type ValidationMode = \"strict\" | \"warn\" | \"ignore\";\n\n/**\n * JSON patch operation types (RFC 6902)\n */\nexport type PatchOp = \"add\" | \"remove\" | \"replace\" | \"move\" | \"copy\" | \"test\";\n\n/**\n * JSON patch operation (RFC 6902)\n */\nexport interface JsonPatch {\n  op: PatchOp;\n  path: string;\n  /** Required for add, replace, test */\n  value?: unknown;\n  /** Required for move, copy (source location) */\n  from?: string;\n}\n\n/**\n * Resolve a dynamic value against a state model\n */\nexport function resolveDynamicValue<T>(\n  value: DynamicValue<T>,\n  stateModel: StateModel,\n): T | undefined {\n  if (value === null || value === undefined) {\n    return undefined;\n  }\n\n  if (typeof value === \"object\" && \"$state\" in value) {\n    return getByPath(stateModel, (value as { $state: string }).$state) as\n      | T\n      | undefined;\n  }\n\n  return value as T;\n}\n\n/**\n * Unescape a JSON Pointer token per RFC 6901 Section 4.\n * ~1 is decoded to / and ~0 is decoded to ~ (order matters).\n */\nfunction unescapeJsonPointer(token: string): string {\n  return token.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n}\n\n/**\n * Parse a JSON Pointer path into unescaped segments.\n */\nexport function parseJsonPointer(path: string): string[] {\n  const raw = path.startsWith(\"/\") ? path.slice(1).split(\"/\") : path.split(\"/\");\n  return raw.map(unescapeJsonPointer);\n}\n\n/**\n * Get a value from an object by JSON Pointer path (RFC 6901)\n */\nexport function getByPath(obj: unknown, path: string): unknown {\n  if (!path || path === \"/\") {\n    return obj;\n  }\n\n  const segments = parseJsonPointer(path);\n\n  let current: unknown = obj;\n\n  for (const segment of segments) {\n    if (current === null || current === undefined) {\n      return undefined;\n    }\n\n    if (Array.isArray(current)) {\n      const index = parseInt(segment, 10);\n      current = current[index];\n    } else if (typeof current === \"object\") {\n      current = (current as Record<string, unknown>)[segment];\n    } else {\n      return undefined;\n    }\n  }\n\n  return current;\n}\n\n/**\n * Check if a string is a numeric index\n */\nfunction isNumericIndex(str: string): boolean {\n  return /^\\d+$/.test(str);\n}\n\n/**\n * Set a value in an object by JSON Pointer path (RFC 6901).\n * Automatically creates arrays when the path segment is a numeric index.\n */\nexport function setByPath(\n  obj: Record<string, unknown>,\n  path: string,\n  value: unknown,\n): void {\n  const segments = parseJsonPointer(path);\n\n  if (segments.length === 0) return;\n\n  let current: Record<string, unknown> | unknown[] = obj;\n\n  for (let i = 0; i < segments.length - 1; i++) {\n    const segment = segments[i]!;\n    const nextSegment = segments[i + 1];\n    const nextIsNumeric =\n      nextSegment !== undefined &&\n      (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n    if (Array.isArray(current)) {\n      const index = parseInt(segment, 10);\n      if (current[index] === undefined || typeof current[index] !== \"object\") {\n        current[index] = nextIsNumeric ? [] : {};\n      }\n      current = current[index] as Record<string, unknown> | unknown[];\n    } else {\n      if (!(segment in current) || typeof current[segment] !== \"object\") {\n        current[segment] = nextIsNumeric ? [] : {};\n      }\n      current = current[segment] as Record<string, unknown> | unknown[];\n    }\n  }\n\n  const lastSegment = segments[segments.length - 1]!;\n  if (Array.isArray(current)) {\n    if (lastSegment === \"-\") {\n      current.push(value);\n    } else {\n      const index = parseInt(lastSegment, 10);\n      current[index] = value;\n    }\n  } else {\n    current[lastSegment] = value;\n  }\n}\n\n/**\n * Add a value per RFC 6902 \"add\" semantics.\n * For objects: create-or-replace the member.\n * For arrays: insert before the given index, or append if \"-\".\n */\nexport function addByPath(\n  obj: Record<string, unknown>,\n  path: string,\n  value: unknown,\n): void {\n  const segments = parseJsonPointer(path);\n\n  if (segments.length === 0) return;\n\n  let current: Record<string, unknown> | unknown[] = obj;\n\n  for (let i = 0; i < segments.length - 1; i++) {\n    const segment = segments[i]!;\n    const nextSegment = segments[i + 1];\n    const nextIsNumeric =\n      nextSegment !== undefined &&\n      (isNumericIndex(nextSegment) || nextSegment === \"-\");\n\n    if (Array.isArray(current)) {\n      const index = parseInt(segment, 10);\n      if (current[index] === undefined || typeof current[index] !== \"object\") {\n        current[index] = nextIsNumeric ? [] : {};\n      }\n      current = current[index] as Record<string, unknown> | unknown[];\n    } else {\n      if (!(segment in current) || typeof current[segment] !== \"object\") {\n        current[segment] = nextIsNumeric ? [] : {};\n      }\n      current = current[segment] as Record<string, unknown> | unknown[];\n    }\n  }\n\n  const lastSegment = segments[segments.length - 1]!;\n  if (Array.isArray(current)) {\n    if (lastSegment === \"-\") {\n      current.push(value);\n    } else {\n      const index = parseInt(lastSegment, 10);\n      current.splice(index, 0, value);\n    }\n  } else {\n    current[lastSegment] = value;\n  }\n}\n\n/**\n * Remove a value per RFC 6902 \"remove\" semantics.\n * For objects: delete the property.\n * For arrays: splice out the element at the given index.\n */\nexport function removeByPath(obj: Record<string, unknown>, path: string): void {\n  const segments = parseJsonPointer(path);\n\n  if (segments.length === 0) return;\n\n  let current: Record<string, unknown> | unknown[] = obj;\n\n  for (let i = 0; i < segments.length - 1; i++) {\n    const segment = segments[i]!;\n\n    if (Array.isArray(current)) {\n      const index = parseInt(segment, 10);\n      if (current[index] === undefined || typeof current[index] !== \"object\") {\n        return; // path does not exist\n      }\n      current = current[index] as Record<string, unknown> | unknown[];\n    } else {\n      if (!(segment in current) || typeof current[segment] !== \"object\") {\n        return; // path does not exist\n      }\n      current = current[segment] as Record<string, unknown> | unknown[];\n    }\n  }\n\n  const lastSegment = segments[segments.length - 1]!;\n  if (Array.isArray(current)) {\n    const index = parseInt(lastSegment, 10);\n    if (index >= 0 && index < current.length) {\n      current.splice(index, 1);\n    }\n  } else {\n    delete current[lastSegment];\n  }\n}\n\n/**\n * Deep equality check for RFC 6902 \"test\" operation.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n  if (a === b) return true;\n  if (a === null || b === null) return false;\n  if (typeof a !== typeof b) return false;\n  if (typeof a !== \"object\") return false;\n\n  if (Array.isArray(a)) {\n    if (!Array.isArray(b)) return false;\n    if (a.length !== b.length) return false;\n    return a.every((item, i) => deepEqual(item, b[i]));\n  }\n\n  const aObj = a as Record<string, unknown>;\n  const bObj = b as Record<string, unknown>;\n  const aKeys = Object.keys(aObj);\n  const bKeys = Object.keys(bObj);\n\n  if (aKeys.length !== bKeys.length) return false;\n  return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));\n}\n\n/**\n * Find a form value from params and/or state.\n * Useful in action handlers to locate form input values regardless of path format.\n *\n * Checks in order:\n * 1. Direct param key (if not a path reference)\n * 2. Param keys ending with the field name\n * 3. State keys ending with the field name (dot notation)\n * 4. State path using getByPath (slash notation)\n *\n * @example\n * // Find \"name\" from params or state\n * const name = findFormValue(\"name\", params, state);\n *\n * // Will find from: params.name, params[\"form.name\"], state[\"form.name\"], or getByPath(state, \"name\")\n */\nexport function findFormValue(\n  fieldName: string,\n  params?: Record<string, unknown>,\n  state?: Record<string, unknown>,\n): unknown {\n  // Check params first (but not if it looks like a state path reference)\n  if (params?.[fieldName] !== undefined) {\n    const val = params[fieldName];\n    // If the value looks like a path reference (contains dots), skip it\n    if (typeof val !== \"string\" || !val.includes(\".\")) {\n      return val;\n    }\n  }\n\n  // Check param keys that end with the field name\n  if (params) {\n    for (const key of Object.keys(params)) {\n      if (key.endsWith(`.${fieldName}`)) {\n        const val = params[key];\n        if (typeof val !== \"string\" || !val.includes(\".\")) {\n          return val;\n        }\n      }\n    }\n  }\n\n  // Check state keys that end with the field name (handles any form naming)\n  if (state) {\n    for (const key of Object.keys(state)) {\n      if (key === fieldName || key.endsWith(`.${fieldName}`)) {\n        return state[key];\n      }\n    }\n\n    // Try getByPath with the raw field name\n    const val = getByPath(state, fieldName);\n    if (val !== undefined) {\n      return val;\n    }\n  }\n\n  return undefined;\n}\n\n// =============================================================================\n// SpecStream - Streaming format for progressively building specs\n// =============================================================================\n\n/**\n * A SpecStream line - a single patch operation in the stream.\n */\nexport type SpecStreamLine = JsonPatch;\n\n/**\n * Parse a single SpecStream line into a patch operation.\n * Returns null if the line is invalid or empty.\n *\n * SpecStream is json-render's streaming format where each line is a JSON patch\n * operation that progressively builds up the final spec.\n */\nexport function parseSpecStreamLine(line: string): SpecStreamLine | null {\n  const trimmed = line.trim();\n  if (!trimmed || !trimmed.startsWith(\"{\")) return null;\n\n  try {\n    const patch = JSON.parse(trimmed) as SpecStreamLine;\n    if (patch.op && patch.path !== undefined) {\n      return patch;\n    }\n    return null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to an object.\n * Mutates the object in place.\n *\n * Supports all six RFC 6902 operations: add, remove, replace, move, copy, test.\n *\n * @throws {Error} If a \"test\" operation fails (value mismatch).\n */\nexport function applySpecStreamPatch<T extends Record<string, unknown>>(\n  obj: T,\n  patch: SpecStreamLine,\n): T {\n  switch (patch.op) {\n    case \"add\":\n      addByPath(obj, patch.path, patch.value);\n      break;\n    case \"replace\":\n      // RFC 6902: target must exist. For streaming tolerance we set regardless.\n      setByPath(obj, patch.path, patch.value);\n      break;\n    case \"remove\":\n      removeByPath(obj, patch.path);\n      break;\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getByPath(obj, patch.from);\n      removeByPath(obj, patch.from);\n      addByPath(obj, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getByPath(obj, patch.from);\n      addByPath(obj, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      const actual = getByPath(obj, patch.path);\n      if (!deepEqual(actual, patch.value)) {\n        throw new Error(\n          `Test operation failed: value at \"${patch.path}\" does not match`,\n        );\n      }\n      break;\n    }\n  }\n  return obj;\n}\n\n/**\n * Apply a single RFC 6902 JSON Patch operation to a Spec.\n * Mutates the spec in place and returns it.\n *\n * This is a typed convenience wrapper around `applySpecStreamPatch` that\n * accepts a `Spec` directly without requiring a cast to `Record<string, unknown>`.\n *\n * Note: This mutates the spec. For React state updates, spread the result\n * to create a new reference: `setSpec({ ...applySpecPatch(spec, patch) })`.\n *\n * @example\n * let spec: Spec = { root: \"\", elements: {} };\n * applySpecPatch(spec, { op: \"add\", path: \"/root\", value: \"main\" });\n */\nexport function applySpecPatch(spec: Spec, patch: SpecStreamLine): Spec {\n  applySpecStreamPatch(spec as unknown as Record<string, unknown>, patch);\n  return spec;\n}\n\n// =============================================================================\n// Nested-to-Flat Conversion\n// =============================================================================\n\n/**\n * A nested spec node. This is the tree format that humans naturally write —\n * each node has inline `children` as an array of child node objects rather\n * than string keys.\n */\ninterface NestedNode {\n  type: string;\n  props: Record<string, unknown>;\n  children?: NestedNode[];\n  /** Any other top-level fields (visible, on, repeat, etc.) */\n  [key: string]: unknown;\n}\n\n/**\n * Convert a nested (tree-structured) spec into the flat `Spec` format used\n * by json-render renderers.\n *\n * In the nested format each node has inline `children` as an array of child\n * objects. This function walks the tree, assigns auto-generated keys\n * (`el-0`, `el-1`, ...), and produces a flat `{ root, elements, state }` spec.\n *\n * The top-level `state` field (if present on the root node) is hoisted to\n * `spec.state`.\n *\n * @example\n * ```ts\n * const nested = {\n *   type: \"Card\",\n *   props: { title: \"Hello\" },\n *   children: [\n *     { type: \"Text\", props: { content: \"World\" } },\n *   ],\n *   state: { count: 0 },\n * };\n * const spec = nestedToFlat(nested);\n * // {\n * //   root: \"el-0\",\n * //   elements: {\n * //     \"el-0\": { type: \"Card\", props: { title: \"Hello\" }, children: [\"el-1\"] },\n * //     \"el-1\": { type: \"Text\", props: { content: \"World\" }, children: [] },\n * //   },\n * //   state: { count: 0 },\n * // }\n * ```\n */\nexport function nestedToFlat(nested: Record<string, unknown>): Spec {\n  const elements: Record<string, UIElement> = {};\n  let counter = 0;\n\n  function walk(node: Record<string, unknown>): string {\n    const key = `el-${counter++}`;\n    const { type, props, children: rawChildren, ...rest } = node as NestedNode;\n\n    // Recursively flatten children\n    const childKeys: string[] = [];\n    if (Array.isArray(rawChildren)) {\n      for (const child of rawChildren) {\n        if (child && typeof child === \"object\" && \"type\" in child) {\n          childKeys.push(walk(child as Record<string, unknown>));\n        }\n      }\n    }\n\n    // Build the flat element, preserving extra fields (visible, on, repeat, etc.)\n    // but excluding `state` which is hoisted to spec-level.\n    const element: UIElement = {\n      type: type ?? \"unknown\",\n      props: (props as Record<string, unknown>) ?? {},\n      children: childKeys,\n    };\n\n    // Copy extra fields (visible, on, repeat) but not state\n    for (const [k, v] of Object.entries(rest)) {\n      if (k !== \"state\" && v !== undefined) {\n        (element as unknown as Record<string, unknown>)[k] = v;\n      }\n    }\n\n    elements[key] = element;\n    return key;\n  }\n\n  const root = walk(nested);\n\n  const spec: Spec = { root, elements };\n\n  // Hoist state from root node if present\n  if (\n    nested.state &&\n    typeof nested.state === \"object\" &&\n    !Array.isArray(nested.state)\n  ) {\n    spec.state = nested.state as Record<string, unknown>;\n  }\n\n  return spec;\n}\n\n/**\n * Compile a SpecStream string into a JSON object.\n * Each line should be a patch operation.\n *\n * @example\n * const stream = `{\"op\":\"add\",\"path\":\"/name\",\"value\":\"Alice\"}\n * {\"op\":\"add\",\"path\":\"/age\",\"value\":30}`;\n * const result = compileSpecStream(stream);\n * // { name: \"Alice\", age: 30 }\n */\nexport function compileSpecStream<\n  T extends Record<string, unknown> = Record<string, unknown>,\n>(stream: string, initial: T = {} as T): T {\n  const lines = stream.split(\"\\n\");\n  const result = { ...initial };\n\n  for (const line of lines) {\n    const patch = parseSpecStreamLine(line);\n    if (patch) {\n      applySpecStreamPatch(result, patch);\n    }\n  }\n\n  return result as T;\n}\n\n/**\n * Streaming SpecStream compiler.\n * Useful for processing SpecStream data as it streams in from AI.\n *\n * @example\n * const compiler = createSpecStreamCompiler<MySpec>();\n *\n * // As chunks arrive:\n * const { result, newPatches } = compiler.push(chunk);\n * if (newPatches.length > 0) {\n *   updateUI(result);\n * }\n *\n * // When done:\n * const finalResult = compiler.getResult();\n */\nexport interface SpecStreamCompiler<T> {\n  /** Push a chunk of text. Returns the current result and any new patches applied. */\n  push(chunk: string): { result: T; newPatches: SpecStreamLine[] };\n  /** Get the current compiled result */\n  getResult(): T;\n  /** Get all patches that have been applied */\n  getPatches(): SpecStreamLine[];\n  /** Reset the compiler to initial state */\n  reset(initial?: Partial<T>): void;\n}\n\n/**\n * Create a streaming SpecStream compiler.\n *\n * SpecStream is json-render's streaming format. AI outputs patch operations\n * line by line, and this compiler progressively builds the final spec.\n *\n * @example\n * const compiler = createSpecStreamCompiler<TimelineSpec>();\n *\n * // Process streaming response\n * const reader = response.body.getReader();\n * while (true) {\n *   const { done, value } = await reader.read();\n *   if (done) break;\n *\n *   const { result, newPatches } = compiler.push(decoder.decode(value));\n *   if (newPatches.length > 0) {\n *     setSpec(result); // Update UI with partial result\n *   }\n * }\n */\nexport function createSpecStreamCompiler<T = Record<string, unknown>>(\n  initial: Partial<T> = {},\n): SpecStreamCompiler<T> {\n  let result = { ...initial } as T;\n  let buffer = \"\";\n  const appliedPatches: SpecStreamLine[] = [];\n  const processedLines = new Set<string>();\n\n  return {\n    push(chunk: string): { result: T; newPatches: SpecStreamLine[] } {\n      buffer += chunk;\n      const newPatches: SpecStreamLine[] = [];\n\n      // Process complete lines\n      const lines = buffer.split(\"\\n\");\n      buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n      for (const line of lines) {\n        const trimmed = line.trim();\n        if (!trimmed || processedLines.has(trimmed)) continue;\n        processedLines.add(trimmed);\n\n        const patch = parseSpecStreamLine(trimmed);\n        if (patch) {\n          applySpecStreamPatch(result as Record<string, unknown>, patch);\n          appliedPatches.push(patch);\n          newPatches.push(patch);\n        }\n      }\n\n      // Return a shallow copy to trigger re-renders\n      if (newPatches.length > 0) {\n        result = { ...result };\n      }\n\n      return { result, newPatches };\n    },\n\n    getResult(): T {\n      // Process any remaining buffer\n      if (buffer.trim()) {\n        const patch = parseSpecStreamLine(buffer);\n        if (patch && !processedLines.has(buffer.trim())) {\n          processedLines.add(buffer.trim());\n          applySpecStreamPatch(result as Record<string, unknown>, patch);\n          appliedPatches.push(patch);\n          result = { ...result };\n        }\n        buffer = \"\";\n      }\n      return result;\n    },\n\n    getPatches(): SpecStreamLine[] {\n      return [...appliedPatches];\n    },\n\n    reset(newInitial: Partial<T> = {}): void {\n      result = { ...newInitial } as T;\n      buffer = \"\";\n      appliedPatches.length = 0;\n      processedLines.clear();\n    },\n  };\n}\n\n// =============================================================================\n// Mixed Stream Parser — for chat + GenUI (text interleaved with JSONL patches)\n// =============================================================================\n\n/**\n * Callbacks for the mixed stream parser.\n */\nexport interface MixedStreamCallbacks {\n  /** Called when a JSONL patch line is parsed */\n  onPatch: (patch: SpecStreamLine) => void;\n  /** Called when a text (non-JSONL) line is received */\n  onText: (text: string) => void;\n}\n\n/**\n * A stateful parser for mixed streams that contain both text and JSONL patches.\n * Used in chat + GenUI scenarios where an LLM responds with conversational text\n * interleaved with json-render JSONL patch operations.\n */\nexport interface MixedStreamParser {\n  /** Push a chunk of streamed data. Calls onPatch/onText for each complete line. */\n  push(chunk: string): void;\n  /** Flush any remaining buffered content. Call when the stream ends. */\n  flush(): void;\n}\n\n/**\n * Create a parser for mixed text + JSONL streams.\n *\n * In chat + GenUI scenarios, an LLM streams a response that contains both\n * conversational text and json-render JSONL patch lines. This parser buffers\n * incoming chunks, splits them into lines, and classifies each line as either\n * a JSONL patch (via `parseSpecStreamLine`) or plain text.\n *\n * @example\n * const parser = createMixedStreamParser({\n *   onText: (text) => appendToMessage(text),\n *   onPatch: (patch) => applySpecPatch(spec, patch),\n * });\n *\n * // As chunks arrive from the stream:\n * for await (const chunk of stream) {\n *   parser.push(chunk);\n * }\n * parser.flush();\n */\nexport function createMixedStreamParser(\n  callbacks: MixedStreamCallbacks,\n): MixedStreamParser {\n  let buffer = \"\";\n  let inSpecFence = false;\n\n  function processLine(line: string): void {\n    const trimmed = line.trim();\n\n    // Fence detection\n    if (!inSpecFence && trimmed.startsWith(\"```spec\")) {\n      inSpecFence = true;\n      return;\n    }\n    if (inSpecFence && trimmed === \"```\") {\n      inSpecFence = false;\n      return;\n    }\n\n    if (!trimmed) return;\n\n    if (inSpecFence) {\n      const patch = parseSpecStreamLine(trimmed);\n      if (patch) {\n        callbacks.onPatch(patch);\n      }\n      return;\n    }\n\n    // Outside fence: heuristic mode\n    const patch = parseSpecStreamLine(trimmed);\n    if (patch) {\n      callbacks.onPatch(patch);\n    } else {\n      callbacks.onText(line);\n    }\n  }\n\n  return {\n    push(chunk: string): void {\n      buffer += chunk;\n\n      // Process complete lines\n      const lines = buffer.split(\"\\n\");\n      buffer = lines.pop() || \"\"; // Keep incomplete line in buffer\n\n      for (const line of lines) {\n        processLine(line);\n      }\n    },\n\n    flush(): void {\n      if (buffer.trim()) {\n        processLine(buffer);\n      }\n      buffer = \"\";\n    },\n  };\n}\n\n// =============================================================================\n// AI SDK Stream Transform\n// =============================================================================\n\n/**\n * Minimal chunk shape compatible with the AI SDK's `UIMessageChunk`.\n *\n * Defined here so that `@json-render/core` has no dependency on the `ai`\n * package. The discriminated union covers the three text-related chunk types\n * the transform inspects; all other chunk types pass through via the fallback.\n */\nexport type StreamChunk =\n  | { type: \"text-start\"; id: string; [k: string]: unknown }\n  | { type: \"text-delta\"; id: string; delta: string; [k: string]: unknown }\n  | { type: \"text-end\"; id: string; [k: string]: unknown }\n  | { type: string; [k: string]: unknown };\n\n/** The opening fence for a spec block (e.g. ` ```spec `). */\nconst SPEC_FENCE_OPEN = \"```spec\";\n/** The closing fence for a spec block. */\nconst SPEC_FENCE_CLOSE = \"```\";\n\n/**\n * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks\n * and classifies text content as either prose or json-render JSONL patches.\n *\n * Two classification modes:\n *\n * 1. **Fence mode** (preferred): Lines between ` ```spec ` and ` ``` ` are\n *    parsed as JSONL patches. Fence delimiters are swallowed (not emitted).\n * 2. **Heuristic mode** (backward compat): Outside of fences, lines starting\n *    with `{` are buffered and tested with `parseSpecStreamLine`. Valid patches\n *    are emitted as {@link SPEC_DATA_PART_TYPE} parts; everything else is\n *    flushed as text.\n *\n * Non-text chunks (tool events, step markers, etc.) are passed through unchanged.\n *\n * @example\n * ```ts\n * import { createJsonRenderTransform } from \"@json-render/core\";\n * import { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n *\n * const stream = createUIMessageStream({\n *   execute: async ({ writer }) => {\n *     writer.merge(\n *       result.toUIMessageStream().pipeThrough(createJsonRenderTransform()),\n *     );\n *   },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function createJsonRenderTransform(): TransformStream<\n  StreamChunk,\n  StreamChunk\n> {\n  let lineBuffer = \"\";\n  let currentTextId = \"\";\n  // Whether the current incomplete line might be JSONL (starts with '{')\n  let buffering = false;\n  // Whether we are inside a ```spec fence\n  let inSpecFence = false;\n  // Whether we are currently inside a text block (between text-start/text-end).\n  // Used to split text blocks around spec data so the AI SDK creates separate\n  // text parts, preserving interleaving of prose and UI in message.parts.\n  let inTextBlock = false;\n  let textIdCounter = 0;\n\n  /** Close the current text block if one is open. */\n  function closeTextBlock(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (inTextBlock) {\n      controller.enqueue({ type: \"text-end\", id: currentTextId });\n      inTextBlock = false;\n    }\n  }\n\n  /** Ensure a text block is open, starting a new one if needed. */\n  function ensureTextBlock(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (!inTextBlock) {\n      textIdCounter++;\n      currentTextId = String(textIdCounter);\n      controller.enqueue({ type: \"text-start\", id: currentTextId });\n      inTextBlock = true;\n    }\n  }\n\n  /** Emit a text-delta, opening a text block first if necessary. */\n  function emitTextDelta(\n    delta: string,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    ensureTextBlock(controller);\n    controller.enqueue({ type: \"text-delta\", id: currentTextId, delta });\n  }\n\n  function emitPatch(\n    patch: SpecStreamLine,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    closeTextBlock(controller);\n    controller.enqueue({\n      type: SPEC_DATA_PART_TYPE,\n      data: { type: \"patch\", patch },\n    });\n  }\n\n  function flushBuffer(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (!lineBuffer) return;\n\n    const trimmed = lineBuffer.trim();\n\n    // Inside a fence, everything is spec data\n    if (inSpecFence) {\n      if (trimmed) {\n        const patch = parseSpecStreamLine(trimmed);\n        if (patch) emitPatch(patch, controller);\n        // Non-patch lines inside the fence are silently dropped\n      }\n      lineBuffer = \"\";\n      buffering = false;\n      return;\n    }\n\n    if (trimmed) {\n      const patch = parseSpecStreamLine(trimmed);\n      if (patch) {\n        emitPatch(patch, controller);\n      } else {\n        // Was buffered but isn't JSONL — flush as text\n        emitTextDelta(lineBuffer, controller);\n      }\n    } else {\n      // Whitespace-only buffer — forward as-is (preserves blank lines)\n      emitTextDelta(lineBuffer, controller);\n    }\n    lineBuffer = \"\";\n    buffering = false;\n  }\n\n  function processCompleteLine(\n    line: string,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    const trimmed = line.trim();\n\n    // --- Fence detection ---\n    if (!inSpecFence && trimmed.startsWith(SPEC_FENCE_OPEN)) {\n      inSpecFence = true;\n      return; // Swallow the opening fence\n    }\n    if (inSpecFence && trimmed === SPEC_FENCE_CLOSE) {\n      inSpecFence = false;\n      return; // Swallow the closing fence\n    }\n\n    // Inside a fence: parse as spec data\n    if (inSpecFence) {\n      if (trimmed) {\n        const patch = parseSpecStreamLine(trimmed);\n        if (patch) emitPatch(patch, controller);\n      }\n      return;\n    }\n\n    // --- Outside fence: heuristic mode ---\n    if (!trimmed) {\n      // Empty line — forward for markdown paragraph breaks\n      emitTextDelta(\"\\n\", controller);\n      return;\n    }\n\n    const patch = parseSpecStreamLine(trimmed);\n    if (patch) {\n      emitPatch(patch, controller);\n    } else {\n      emitTextDelta(line + \"\\n\", controller);\n    }\n  }\n\n  return new TransformStream<StreamChunk, StreamChunk>({\n    transform(chunk, controller) {\n      switch (chunk.type) {\n        case \"text-start\": {\n          const id = (chunk as { id: string }).id;\n          const idNum = parseInt(id, 10);\n          if (!isNaN(idNum) && idNum >= textIdCounter) {\n            textIdCounter = idNum;\n          }\n          currentTextId = id;\n          inTextBlock = true;\n          controller.enqueue(chunk);\n          break;\n        }\n\n        case \"text-delta\": {\n          const delta = chunk as { id: string; delta: string };\n          const text = delta.delta;\n\n          for (let i = 0; i < text.length; i++) {\n            const ch = text.charAt(i);\n\n            if (ch === \"\\n\") {\n              // Line complete — classify and emit\n              if (buffering) {\n                processCompleteLine(lineBuffer, controller);\n                lineBuffer = \"\";\n                buffering = false;\n              } else {\n                // Outside fence, emit newline; inside fence, swallow it\n                if (!inSpecFence) {\n                  emitTextDelta(\"\\n\", controller);\n                }\n              }\n            } else if (lineBuffer.length === 0 && !buffering) {\n              // Start of a new line — decide whether to buffer or stream\n              if (inSpecFence || ch === \"{\" || ch === \"`\") {\n                // Buffer: inside fence (everything), or heuristic mode ({), or potential fence (`)\n                buffering = true;\n                lineBuffer += ch;\n              } else {\n                emitTextDelta(ch, controller);\n              }\n            } else if (buffering) {\n              lineBuffer += ch;\n            } else {\n              emitTextDelta(ch, controller);\n            }\n          }\n          break;\n        }\n\n        case \"text-end\": {\n          flushBuffer(controller);\n          if (inTextBlock) {\n            controller.enqueue({ type: \"text-end\", id: currentTextId });\n            inTextBlock = false;\n          }\n          break;\n        }\n\n        default: {\n          controller.enqueue(chunk);\n          break;\n        }\n      }\n    },\n\n    flush(controller) {\n      flushBuffer(controller);\n      closeTextBlock(controller);\n    },\n  });\n}\n\n/**\n * The key registered in `AppDataParts` for json-render specs.\n * The AI SDK automatically prefixes this with `\"data-\"` on the wire,\n * so the actual stream chunk type is `\"data-spec\"` (see {@link SPEC_DATA_PART_TYPE}).\n *\n * @example\n * ```ts\n * import { SPEC_DATA_PART, type SpecDataPart } from \"@json-render/core\";\n * type AppDataParts = { [SPEC_DATA_PART]: SpecDataPart };\n * ```\n */\nexport const SPEC_DATA_PART = \"spec\" as const;\n\n/**\n * The wire-format type string as it appears in stream chunks and message parts.\n * This is `\"data-\"` + {@link SPEC_DATA_PART} — i.e. `\"data-spec\"`.\n *\n * Use this constant when filtering message parts or enqueuing stream chunks.\n */\nexport const SPEC_DATA_PART_TYPE = `data-${SPEC_DATA_PART}` as const;\n\n/**\n * Discriminated union for the payload of a {@link SPEC_DATA_PART_TYPE} SSE part.\n *\n * - `\"patch\"`: A single RFC 6902 JSON Patch operation (streaming, progressive UI).\n * - `\"flat\"`: A complete flat spec with `root`, `elements`, and optional `state`.\n * - `\"nested\"`: A complete nested spec (tree structure — schema depends on catalog).\n */\nexport type SpecDataPart =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"flat\"; spec: Spec }\n  | { type: \"nested\"; spec: Record<string, unknown> };\n\n/**\n * Convenience wrapper that pipes an AI SDK UI message stream through the\n * json-render transform, classifying text as prose or JSONL patches.\n *\n * Eliminates the need for manual `pipeThrough(createJsonRenderTransform())`\n * and the associated type cast.\n *\n * @example\n * ```ts\n * import { pipeJsonRender } from \"@json-render/core\";\n *\n * const stream = createUIMessageStream({\n *   execute: async ({ writer }) => {\n *     writer.merge(pipeJsonRender(result.toUIMessageStream()));\n *   },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function pipeJsonRender<T = StreamChunk>(\n  stream: ReadableStream<T>,\n): ReadableStream<T> {\n  return stream.pipeThrough(\n    createJsonRenderTransform() as unknown as TransformStream<T, T>,\n  );\n}\n"
  },
  {
    "path": "packages/core/src/validation.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport {\n  builtInValidationFunctions,\n  runValidationCheck,\n  runValidation,\n  check,\n} from \"./validation\";\n\ndescribe(\"builtInValidationFunctions\", () => {\n  describe(\"required\", () => {\n    it(\"passes for non-empty values\", () => {\n      expect(builtInValidationFunctions.required(\"hello\")).toBe(true);\n      expect(builtInValidationFunctions.required(0)).toBe(true);\n      expect(builtInValidationFunctions.required(false)).toBe(true);\n      expect(builtInValidationFunctions.required([\"item\"])).toBe(true);\n      expect(builtInValidationFunctions.required({ key: \"value\" })).toBe(true);\n    });\n\n    it(\"fails for empty values\", () => {\n      expect(builtInValidationFunctions.required(\"\")).toBe(false);\n      expect(builtInValidationFunctions.required(\"   \")).toBe(false);\n      expect(builtInValidationFunctions.required(null)).toBe(false);\n      expect(builtInValidationFunctions.required(undefined)).toBe(false);\n      expect(builtInValidationFunctions.required([])).toBe(false);\n    });\n  });\n\n  describe(\"email\", () => {\n    it(\"passes for valid emails\", () => {\n      expect(builtInValidationFunctions.email(\"test@example.com\")).toBe(true);\n      expect(builtInValidationFunctions.email(\"user.name@domain.co\")).toBe(\n        true,\n      );\n      expect(builtInValidationFunctions.email(\"a@b.c\")).toBe(true);\n    });\n\n    it(\"fails for invalid emails\", () => {\n      expect(builtInValidationFunctions.email(\"invalid\")).toBe(false);\n      expect(builtInValidationFunctions.email(\"missing@domain\")).toBe(false);\n      expect(builtInValidationFunctions.email(\"@domain.com\")).toBe(false);\n      expect(builtInValidationFunctions.email(\"user@\")).toBe(false);\n      expect(builtInValidationFunctions.email(123)).toBe(false);\n    });\n  });\n\n  describe(\"minLength\", () => {\n    it(\"passes when string meets minimum length\", () => {\n      expect(builtInValidationFunctions.minLength(\"hello\", { min: 3 })).toBe(\n        true,\n      );\n      expect(builtInValidationFunctions.minLength(\"abc\", { min: 3 })).toBe(\n        true,\n      );\n      expect(builtInValidationFunctions.minLength(\"abcdef\", { min: 3 })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails when string is too short\", () => {\n      expect(builtInValidationFunctions.minLength(\"hi\", { min: 3 })).toBe(\n        false,\n      );\n      expect(builtInValidationFunctions.minLength(\"\", { min: 1 })).toBe(false);\n    });\n\n    it(\"fails for non-strings\", () => {\n      expect(builtInValidationFunctions.minLength(123, { min: 1 })).toBe(false);\n    });\n\n    it(\"fails when min is not provided\", () => {\n      expect(builtInValidationFunctions.minLength(\"hello\", {})).toBe(false);\n    });\n  });\n\n  describe(\"maxLength\", () => {\n    it(\"passes when string meets maximum length\", () => {\n      expect(builtInValidationFunctions.maxLength(\"hi\", { max: 5 })).toBe(true);\n      expect(builtInValidationFunctions.maxLength(\"hello\", { max: 5 })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails when string exceeds maximum\", () => {\n      expect(builtInValidationFunctions.maxLength(\"hello!\", { max: 5 })).toBe(\n        false,\n      );\n    });\n  });\n\n  describe(\"pattern\", () => {\n    it(\"passes when string matches pattern\", () => {\n      expect(\n        builtInValidationFunctions.pattern(\"abc123\", {\n          pattern: \"^[a-z0-9]+$\",\n        }),\n      ).toBe(true);\n    });\n\n    it(\"fails when string does not match pattern\", () => {\n      expect(\n        builtInValidationFunctions.pattern(\"ABC\", { pattern: \"^[a-z]+$\" }),\n      ).toBe(false);\n    });\n\n    it(\"fails for invalid regex pattern\", () => {\n      expect(\n        builtInValidationFunctions.pattern(\"test\", { pattern: \"[invalid\" }),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"min\", () => {\n    it(\"passes when number meets minimum\", () => {\n      expect(builtInValidationFunctions.min(5, { min: 3 })).toBe(true);\n      expect(builtInValidationFunctions.min(3, { min: 3 })).toBe(true);\n    });\n\n    it(\"fails when number is below minimum\", () => {\n      expect(builtInValidationFunctions.min(2, { min: 3 })).toBe(false);\n    });\n\n    it(\"fails for non-numbers\", () => {\n      expect(builtInValidationFunctions.min(\"5\", { min: 3 })).toBe(false);\n    });\n  });\n\n  describe(\"max\", () => {\n    it(\"passes when number meets maximum\", () => {\n      expect(builtInValidationFunctions.max(3, { max: 5 })).toBe(true);\n      expect(builtInValidationFunctions.max(5, { max: 5 })).toBe(true);\n    });\n\n    it(\"fails when number exceeds maximum\", () => {\n      expect(builtInValidationFunctions.max(6, { max: 5 })).toBe(false);\n    });\n  });\n\n  describe(\"numeric\", () => {\n    it(\"passes for numbers\", () => {\n      expect(builtInValidationFunctions.numeric(42)).toBe(true);\n      expect(builtInValidationFunctions.numeric(3.14)).toBe(true);\n      expect(builtInValidationFunctions.numeric(0)).toBe(true);\n    });\n\n    it(\"passes for numeric strings\", () => {\n      expect(builtInValidationFunctions.numeric(\"42\")).toBe(true);\n      expect(builtInValidationFunctions.numeric(\"3.14\")).toBe(true);\n    });\n\n    it(\"fails for non-numeric values\", () => {\n      expect(builtInValidationFunctions.numeric(\"abc\")).toBe(false);\n      expect(builtInValidationFunctions.numeric(NaN)).toBe(false);\n      expect(builtInValidationFunctions.numeric(null)).toBe(false);\n    });\n  });\n\n  describe(\"url\", () => {\n    it(\"passes for valid URLs\", () => {\n      expect(builtInValidationFunctions.url(\"https://example.com\")).toBe(true);\n      expect(builtInValidationFunctions.url(\"http://localhost:3000\")).toBe(\n        true,\n      );\n      expect(\n        builtInValidationFunctions.url(\"https://example.com/path?query=1\"),\n      ).toBe(true);\n    });\n\n    it(\"fails for invalid URLs\", () => {\n      expect(builtInValidationFunctions.url(\"not-a-url\")).toBe(false);\n      expect(builtInValidationFunctions.url(\"example.com\")).toBe(false);\n    });\n  });\n\n  describe(\"matches\", () => {\n    it(\"passes when values match\", () => {\n      expect(\n        builtInValidationFunctions.matches(\"password\", { other: \"password\" }),\n      ).toBe(true);\n      expect(builtInValidationFunctions.matches(123, { other: 123 })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails when values do not match\", () => {\n      expect(\n        builtInValidationFunctions.matches(\"password\", { other: \"different\" }),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"equalTo\", () => {\n    it(\"passes when values are equal\", () => {\n      expect(builtInValidationFunctions.equalTo(\"abc\", { other: \"abc\" })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails when values differ\", () => {\n      expect(builtInValidationFunctions.equalTo(\"abc\", { other: \"xyz\" })).toBe(\n        false,\n      );\n    });\n  });\n\n  describe(\"lessThan\", () => {\n    it(\"passes when value is less than other\", () => {\n      expect(builtInValidationFunctions.lessThan(3, { other: 5 })).toBe(true);\n    });\n\n    it(\"fails when value equals other\", () => {\n      expect(builtInValidationFunctions.lessThan(5, { other: 5 })).toBe(false);\n    });\n\n    it(\"fails when value is greater than other\", () => {\n      expect(builtInValidationFunctions.lessThan(7, { other: 5 })).toBe(false);\n    });\n\n    it(\"coerces numeric string vs number\", () => {\n      expect(builtInValidationFunctions.lessThan(\"3\", { other: 5 })).toBe(true);\n    });\n\n    it(\"fails coercion when non-numeric string\", () => {\n      expect(builtInValidationFunctions.lessThan(\"abc\", { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"passes for string comparison (ISO dates)\", () => {\n      expect(\n        builtInValidationFunctions.lessThan(\"2026-01-01\", {\n          other: \"2026-06-15\",\n        }),\n      ).toBe(true);\n    });\n\n    it(\"fails for equal strings\", () => {\n      expect(\n        builtInValidationFunctions.lessThan(\"2026-01-01\", {\n          other: \"2026-01-01\",\n        }),\n      ).toBe(false);\n    });\n\n    it(\"returns false when value is empty string\", () => {\n      expect(builtInValidationFunctions.lessThan(\"\", { other: 5 })).toBe(false);\n    });\n\n    it(\"returns false when other is empty string\", () => {\n      expect(builtInValidationFunctions.lessThan(3, { other: \"\" })).toBe(false);\n    });\n\n    it(\"returns false when value is empty string vs non-empty string\", () => {\n      expect(builtInValidationFunctions.lessThan(\"\", { other: \"abc\" })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is empty string vs non-empty string\", () => {\n      expect(builtInValidationFunctions.lessThan(\"abc\", { other: \"\" })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is null\", () => {\n      expect(builtInValidationFunctions.lessThan(3, { other: null })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when value is null\", () => {\n      expect(builtInValidationFunctions.lessThan(null, { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is undefined\", () => {\n      expect(builtInValidationFunctions.lessThan(3, { other: undefined })).toBe(\n        false,\n      );\n    });\n  });\n\n  describe(\"greaterThan\", () => {\n    it(\"passes when value is greater than other\", () => {\n      expect(builtInValidationFunctions.greaterThan(7, { other: 5 })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails when value equals other\", () => {\n      expect(builtInValidationFunctions.greaterThan(5, { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"fails when value is less than other\", () => {\n      expect(builtInValidationFunctions.greaterThan(3, { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"coerces numeric string vs number\", () => {\n      expect(builtInValidationFunctions.greaterThan(\"7\", { other: 5 })).toBe(\n        true,\n      );\n    });\n\n    it(\"fails coercion when non-numeric string\", () => {\n      expect(builtInValidationFunctions.greaterThan(\"abc\", { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"passes for string comparison (ISO dates)\", () => {\n      expect(\n        builtInValidationFunctions.greaterThan(\"2026-06-15\", {\n          other: \"2026-01-01\",\n        }),\n      ).toBe(true);\n    });\n\n    it(\"fails for lesser strings\", () => {\n      expect(\n        builtInValidationFunctions.greaterThan(\"2026-01-01\", {\n          other: \"2026-06-15\",\n        }),\n      ).toBe(false);\n    });\n\n    it(\"returns false when value is empty string\", () => {\n      expect(builtInValidationFunctions.greaterThan(\"\", { other: 5 })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is empty string\", () => {\n      expect(builtInValidationFunctions.greaterThan(3, { other: \"\" })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when value is empty string vs non-empty string\", () => {\n      expect(builtInValidationFunctions.greaterThan(\"\", { other: \"abc\" })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is empty string vs non-empty string\", () => {\n      expect(builtInValidationFunctions.greaterThan(\"abc\", { other: \"\" })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when other is null\", () => {\n      expect(builtInValidationFunctions.greaterThan(3, { other: null })).toBe(\n        false,\n      );\n    });\n\n    it(\"returns false when value is undefined\", () => {\n      expect(\n        builtInValidationFunctions.greaterThan(undefined, { other: 5 }),\n      ).toBe(false);\n    });\n\n    it(\"returns false when value is null\", () => {\n      expect(builtInValidationFunctions.greaterThan(null, { other: 5 })).toBe(\n        false,\n      );\n    });\n  });\n\n  describe(\"requiredIf\", () => {\n    it(\"passes when condition is falsy (field not required)\", () => {\n      expect(builtInValidationFunctions.requiredIf(\"\", { field: false })).toBe(\n        true,\n      );\n      expect(builtInValidationFunctions.requiredIf(\"\", { field: \"\" })).toBe(\n        true,\n      );\n      expect(builtInValidationFunctions.requiredIf(\"\", { field: null })).toBe(\n        true,\n      );\n      expect(\n        builtInValidationFunctions.requiredIf(\"\", { field: undefined }),\n      ).toBe(true);\n    });\n\n    it(\"fails when condition is truthy and value is empty\", () => {\n      expect(builtInValidationFunctions.requiredIf(\"\", { field: true })).toBe(\n        false,\n      );\n      expect(\n        builtInValidationFunctions.requiredIf(null, { field: \"yes\" }),\n      ).toBe(false);\n      expect(\n        builtInValidationFunctions.requiredIf(undefined, { field: 1 }),\n      ).toBe(false);\n    });\n\n    it(\"passes when condition is truthy and value is present\", () => {\n      expect(\n        builtInValidationFunctions.requiredIf(\"hello\", { field: true }),\n      ).toBe(true);\n      expect(builtInValidationFunctions.requiredIf(42, { field: true })).toBe(\n        true,\n      );\n    });\n  });\n});\n\ndescribe(\"runValidationCheck\", () => {\n  it(\"runs a validation check and returns result\", () => {\n    const result = runValidationCheck(\n      { type: \"required\", message: \"Required\" },\n      { value: \"hello\", stateModel: {} },\n    );\n\n    expect(result.type).toBe(\"required\");\n    expect(result.valid).toBe(true);\n    expect(result.message).toBe(\"Required\");\n  });\n\n  it(\"resolves dynamic args from stateModel\", () => {\n    const result = runValidationCheck(\n      {\n        type: \"minLength\",\n        args: { min: { $state: \"/minLen\" } },\n        message: \"Too short\",\n      },\n      { value: \"hi\", stateModel: { minLen: 5 } },\n    );\n\n    expect(result.valid).toBe(false);\n  });\n\n  it(\"returns valid for unknown functions with warning\", () => {\n    const result = runValidationCheck(\n      { type: \"unknownFunction\", message: \"Unknown\" },\n      { value: \"test\", stateModel: {} },\n    );\n\n    expect(result.valid).toBe(true);\n  });\n\n  it(\"uses custom validation functions\", () => {\n    const customFunctions = {\n      startsWithA: (value: unknown) =>\n        typeof value === \"string\" && value.startsWith(\"A\"),\n    };\n\n    const result = runValidationCheck(\n      { type: \"startsWithA\", message: \"Must start with A\" },\n      { value: \"Apple\", stateModel: {}, customFunctions },\n    );\n\n    expect(result.valid).toBe(true);\n  });\n});\n\ndescribe(\"runValidation\", () => {\n  it(\"runs all validation checks\", () => {\n    const result = runValidation(\n      {\n        checks: [\n          { type: \"required\", message: \"Required\" },\n          { type: \"email\", message: \"Invalid email\" },\n        ],\n      },\n      { value: \"test@example.com\", stateModel: {} },\n    );\n\n    expect(result.valid).toBe(true);\n    expect(result.errors).toHaveLength(0);\n    expect(result.checks).toHaveLength(2);\n  });\n\n  it(\"collects all errors\", () => {\n    const result = runValidation(\n      {\n        checks: [\n          { type: \"required\", message: \"Required\" },\n          { type: \"email\", message: \"Invalid email\" },\n        ],\n      },\n      { value: \"\", stateModel: {} },\n    );\n\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain(\"Required\");\n    expect(result.errors).toContain(\"Invalid email\");\n  });\n\n  it(\"skips validation when enabled condition is false\", () => {\n    const result = runValidation(\n      {\n        checks: [{ type: \"required\", message: \"Required\" }],\n        enabled: { $state: \"/enabled\", eq: true }, // False because /enabled is not set\n      },\n      { value: \"\", stateModel: {} },\n    );\n\n    expect(result.valid).toBe(true);\n    expect(result.checks).toHaveLength(0);\n  });\n\n  it(\"runs validation when enabled condition is true\", () => {\n    const result = runValidation(\n      {\n        checks: [{ type: \"required\", message: \"Required\" }],\n        enabled: { $state: \"/enabled\" }, // True because /enabled is truthy\n      },\n      { value: \"\", stateModel: { enabled: true } },\n    );\n\n    expect(result.valid).toBe(false);\n  });\n\n  it(\"returns valid when no checks defined\", () => {\n    const result = runValidation({}, { value: \"\", stateModel: {} });\n\n    expect(result.valid).toBe(true);\n    expect(result.checks).toHaveLength(0);\n  });\n});\n\ndescribe(\"check helper\", () => {\n  describe(\"required\", () => {\n    it(\"creates required check with default message\", () => {\n      const c = check.required();\n\n      expect(c.type).toBe(\"required\");\n      expect(c.message).toBe(\"This field is required\");\n    });\n\n    it(\"creates required check with custom message\", () => {\n      const c = check.required(\"Custom message\");\n\n      expect(c.message).toBe(\"Custom message\");\n    });\n  });\n\n  describe(\"email\", () => {\n    it(\"creates email check with default message\", () => {\n      const c = check.email();\n\n      expect(c.type).toBe(\"email\");\n      expect(c.message).toBe(\"Invalid email address\");\n    });\n  });\n\n  describe(\"minLength\", () => {\n    it(\"creates minLength check with args\", () => {\n      const c = check.minLength(5, \"Too short\");\n\n      expect(c.type).toBe(\"minLength\");\n      expect(c.args).toEqual({ min: 5 });\n      expect(c.message).toBe(\"Too short\");\n    });\n\n    it(\"uses default message\", () => {\n      const c = check.minLength(3);\n\n      expect(c.message).toBe(\"Must be at least 3 characters\");\n    });\n  });\n\n  describe(\"maxLength\", () => {\n    it(\"creates maxLength check with args\", () => {\n      const c = check.maxLength(100);\n\n      expect(c.type).toBe(\"maxLength\");\n      expect(c.args).toEqual({ max: 100 });\n    });\n  });\n\n  describe(\"pattern\", () => {\n    it(\"creates pattern check\", () => {\n      const c = check.pattern(\"^[a-z]+$\", \"Letters only\");\n\n      expect(c.type).toBe(\"pattern\");\n      expect(c.args).toEqual({ pattern: \"^[a-z]+$\" });\n      expect(c.message).toBe(\"Letters only\");\n    });\n  });\n\n  describe(\"min\", () => {\n    it(\"creates min check\", () => {\n      const c = check.min(0, \"Must be positive\");\n\n      expect(c.type).toBe(\"min\");\n      expect(c.args).toEqual({ min: 0 });\n    });\n  });\n\n  describe(\"max\", () => {\n    it(\"creates max check\", () => {\n      const c = check.max(100);\n\n      expect(c.type).toBe(\"max\");\n      expect(c.args).toEqual({ max: 100 });\n    });\n  });\n\n  describe(\"url\", () => {\n    it(\"creates url check\", () => {\n      const c = check.url(\"Must be a URL\");\n\n      expect(c.type).toBe(\"url\");\n      expect(c.message).toBe(\"Must be a URL\");\n    });\n  });\n\n  describe(\"numeric\", () => {\n    it(\"creates numeric check with default message\", () => {\n      const c = check.numeric();\n\n      expect(c.type).toBe(\"numeric\");\n      expect(c.message).toBe(\"Must be a number\");\n    });\n\n    it(\"creates numeric check with custom message\", () => {\n      const c = check.numeric(\"Numbers only\");\n\n      expect(c.type).toBe(\"numeric\");\n      expect(c.message).toBe(\"Numbers only\");\n    });\n  });\n\n  describe(\"matches\", () => {\n    it(\"creates matches check with path reference\", () => {\n      const c = check.matches(\"/password\", \"Passwords must match\");\n\n      expect(c.type).toBe(\"matches\");\n      expect(c.args).toEqual({ other: { $state: \"/password\" } });\n      expect(c.message).toBe(\"Passwords must match\");\n    });\n  });\n\n  describe(\"equalTo\", () => {\n    it(\"creates equalTo check with path reference\", () => {\n      const c = check.equalTo(\"/email\", \"Emails must match\");\n\n      expect(c.type).toBe(\"equalTo\");\n      expect(c.args).toEqual({ other: { $state: \"/email\" } });\n      expect(c.message).toBe(\"Emails must match\");\n    });\n  });\n\n  describe(\"lessThan\", () => {\n    it(\"creates lessThan check with path reference\", () => {\n      const c = check.lessThan(\"/maxValue\", \"Must be less\");\n\n      expect(c.type).toBe(\"lessThan\");\n      expect(c.args).toEqual({ other: { $state: \"/maxValue\" } });\n      expect(c.message).toBe(\"Must be less\");\n    });\n  });\n\n  describe(\"greaterThan\", () => {\n    it(\"creates greaterThan check with path reference\", () => {\n      const c = check.greaterThan(\"/minValue\");\n\n      expect(c.type).toBe(\"greaterThan\");\n      expect(c.args).toEqual({ other: { $state: \"/minValue\" } });\n    });\n  });\n\n  describe(\"requiredIf\", () => {\n    it(\"creates requiredIf check with path reference\", () => {\n      const c = check.requiredIf(\"/toggle\", \"Required when toggle is on\");\n\n      expect(c.type).toBe(\"requiredIf\");\n      expect(c.args).toEqual({ field: { $state: \"/toggle\" } });\n      expect(c.message).toBe(\"Required when toggle is on\");\n    });\n  });\n});\n\n// =============================================================================\n// Deep arg resolution in runValidationCheck\n// =============================================================================\n\ndescribe(\"deep arg resolution\", () => {\n  it(\"resolves nested $state refs in validation args\", () => {\n    const result = runValidationCheck(\n      {\n        type: \"matches\",\n        args: { other: { $state: \"/form/password\" } },\n        message: \"Passwords must match\",\n      },\n      {\n        value: \"secret123\",\n        stateModel: { form: { password: \"secret123\" } },\n      },\n    );\n    expect(result.valid).toBe(true);\n  });\n\n  it(\"resolves $state in cross-field lessThan check\", () => {\n    const result = runValidationCheck(\n      {\n        type: \"lessThan\",\n        args: { other: { $state: \"/form/maxPrice\" } },\n        message: \"Must be less than max price\",\n      },\n      {\n        value: 50,\n        stateModel: { form: { maxPrice: 100 } },\n      },\n    );\n    expect(result.valid).toBe(true);\n  });\n\n  it(\"resolves $state in requiredIf check\", () => {\n    const result = runValidationCheck(\n      {\n        type: \"requiredIf\",\n        args: { field: { $state: \"/form/enableEmail\" } },\n        message: \"Email is required\",\n      },\n      {\n        value: \"\",\n        stateModel: { form: { enableEmail: true } },\n      },\n    );\n    expect(result.valid).toBe(false);\n  });\n\n  it(\"passes requiredIf when condition is false\", () => {\n    const result = runValidationCheck(\n      {\n        type: \"requiredIf\",\n        args: { field: { $state: \"/form/enableEmail\" } },\n        message: \"Email is required\",\n      },\n      {\n        value: \"\",\n        stateModel: { form: { enableEmail: false } },\n      },\n    );\n    expect(result.valid).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/core/src/validation.ts",
    "content": "import { z } from \"zod\";\nimport type { DynamicValue, StateModel, VisibilityCondition } from \"./types\";\nimport { DynamicValueSchema, resolveDynamicValue } from \"./types\";\nimport { VisibilityConditionSchema, evaluateVisibility } from \"./visibility\";\nimport { resolvePropValue } from \"./props\";\n\n/**\n * Validation check definition\n */\nexport interface ValidationCheck {\n  /** Validation type (built-in or from catalog) */\n  type: string;\n  /** Additional arguments for the validation */\n  args?: Record<string, DynamicValue>;\n  /** Error message to display if check fails */\n  message: string;\n}\n\n/**\n * Validation configuration for a field\n */\nexport interface ValidationConfig {\n  /** Array of checks to run */\n  checks?: ValidationCheck[];\n  /** When to run validation */\n  validateOn?: \"change\" | \"blur\" | \"submit\";\n  /** Condition for when validation is enabled */\n  enabled?: VisibilityCondition;\n}\n\n/**\n * Schema for validation check\n */\nexport const ValidationCheckSchema = z.object({\n  type: z.string(),\n  args: z.record(z.string(), DynamicValueSchema).optional(),\n  message: z.string(),\n});\n\n/**\n * Schema for validation config\n */\nexport const ValidationConfigSchema = z.object({\n  checks: z.array(ValidationCheckSchema).optional(),\n  validateOn: z.enum([\"change\", \"blur\", \"submit\"]).optional(),\n  enabled: VisibilityConditionSchema.optional(),\n});\n\n/**\n * Validation function signature\n */\nexport type ValidationFunction = (\n  value: unknown,\n  args?: Record<string, unknown>,\n) => boolean;\n\n/**\n * Validation function definition in catalog\n */\nexport interface ValidationFunctionDefinition {\n  /** The validation function */\n  validate: ValidationFunction;\n  /** Description for AI */\n  description?: string;\n}\n\nconst matchesImpl: ValidationFunction = (\n  value: unknown,\n  args?: Record<string, unknown>,\n) => {\n  const other = args?.other;\n  return value === other;\n};\n\n/**\n * Built-in validation functions\n */\nexport const builtInValidationFunctions: Record<string, ValidationFunction> = {\n  /**\n   * Check if value is not null, undefined, or empty string\n   */\n  required: (value: unknown) => {\n    if (value === null || value === undefined) return false;\n    if (typeof value === \"string\") return value.trim().length > 0;\n    if (Array.isArray(value)) return value.length > 0;\n    return true;\n  },\n\n  /**\n   * Check if value is a valid email address\n   */\n  email: (value: unknown) => {\n    if (typeof value !== \"string\") return false;\n    return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value);\n  },\n\n  /**\n   * Check minimum string length\n   */\n  minLength: (value: unknown, args?: Record<string, unknown>) => {\n    if (typeof value !== \"string\") return false;\n    const min = args?.min;\n    if (typeof min !== \"number\") return false;\n    return value.length >= min;\n  },\n\n  /**\n   * Check maximum string length\n   */\n  maxLength: (value: unknown, args?: Record<string, unknown>) => {\n    if (typeof value !== \"string\") return false;\n    const max = args?.max;\n    if (typeof max !== \"number\") return false;\n    return value.length <= max;\n  },\n\n  /**\n   * Check if string matches a regex pattern\n   */\n  pattern: (value: unknown, args?: Record<string, unknown>) => {\n    if (typeof value !== \"string\") return false;\n    const pattern = args?.pattern;\n    if (typeof pattern !== \"string\") return false;\n    try {\n      return new RegExp(pattern).test(value);\n    } catch {\n      return false;\n    }\n  },\n\n  /**\n   * Check minimum numeric value\n   */\n  min: (value: unknown, args?: Record<string, unknown>) => {\n    if (typeof value !== \"number\") return false;\n    const min = args?.min;\n    if (typeof min !== \"number\") return false;\n    return value >= min;\n  },\n\n  /**\n   * Check maximum numeric value\n   */\n  max: (value: unknown, args?: Record<string, unknown>) => {\n    if (typeof value !== \"number\") return false;\n    const max = args?.max;\n    if (typeof max !== \"number\") return false;\n    return value <= max;\n  },\n\n  /**\n   * Check if value is a number\n   */\n  numeric: (value: unknown) => {\n    if (typeof value === \"number\") return !isNaN(value);\n    if (typeof value === \"string\") return !isNaN(parseFloat(value));\n    return false;\n  },\n\n  /**\n   * Check if value is a valid URL\n   */\n  url: (value: unknown) => {\n    if (typeof value !== \"string\") return false;\n    try {\n      new URL(value);\n      return true;\n    } catch {\n      return false;\n    }\n  },\n\n  /**\n   * Check if value matches another field\n   */\n  matches: matchesImpl,\n\n  /**\n   * Alias for matches with a more descriptive name for cross-field equality\n   */\n  equalTo: matchesImpl,\n\n  /**\n   * Check if value is less than another field's value.\n   * Supports numbers, strings (useful for ISO date comparison), and\n   * cross-type numeric coercion (e.g. string \"3\" vs number 5).\n   */\n  lessThan: (value: unknown, args?: Record<string, unknown>) => {\n    const other = args?.other;\n    if (value == null || other == null || value === \"\" || other === \"\")\n      return false;\n    if (typeof value === \"number\" && typeof other === \"number\")\n      return value < other;\n    if (typeof value === \"string\" && typeof other === \"string\")\n      return value < other;\n    const numVal = Number(value);\n    const numOther = Number(other);\n    if (!isNaN(numVal) && !isNaN(numOther)) return numVal < numOther;\n    return false;\n  },\n\n  /**\n   * Check if value is greater than another field's value.\n   * Supports numbers, strings (useful for ISO date comparison), and\n   * cross-type numeric coercion (e.g. string \"7\" vs number 5).\n   */\n  greaterThan: (value: unknown, args?: Record<string, unknown>) => {\n    const other = args?.other;\n    if (value == null || other == null || value === \"\" || other === \"\")\n      return false;\n    if (typeof value === \"number\" && typeof other === \"number\")\n      return value > other;\n    if (typeof value === \"string\" && typeof other === \"string\")\n      return value > other;\n    const numVal = Number(value);\n    const numOther = Number(other);\n    if (!isNaN(numVal) && !isNaN(numOther)) return numVal > numOther;\n    return false;\n  },\n\n  /**\n   * Required only when a condition is met.\n   * Uses JS truthiness: 0, false, \"\", null, and undefined are all\n   * treated as \"condition not met\" (field not required), matching\n   * the visibility system's bare-condition semantics.\n   */\n  requiredIf: (value: unknown, args?: Record<string, unknown>) => {\n    const condition = args?.field;\n    if (!condition) return true;\n    if (value === null || value === undefined) return false;\n    if (typeof value === \"string\") return value.trim().length > 0;\n    if (Array.isArray(value)) return value.length > 0;\n    return true;\n  },\n};\n\n/**\n * Validation result for a single check\n */\nexport interface ValidationCheckResult {\n  type: string;\n  valid: boolean;\n  message: string;\n}\n\n/**\n * Full validation result for a field\n */\nexport interface ValidationResult {\n  valid: boolean;\n  errors: string[];\n  checks: ValidationCheckResult[];\n}\n\n/**\n * Context for running validation\n */\nexport interface ValidationContext {\n  /** Current value to validate */\n  value: unknown;\n  /** Full data model for resolving paths */\n  stateModel: StateModel;\n  /** Custom validation functions from catalog */\n  customFunctions?: Record<string, ValidationFunction>;\n}\n\n/**\n * Run a single validation check\n */\nexport function runValidationCheck(\n  check: ValidationCheck,\n  ctx: ValidationContext,\n): ValidationCheckResult {\n  const { value, stateModel, customFunctions } = ctx;\n\n  // Resolve args using resolvePropValue so nested $state refs (and any other\n  // prop expressions) are handled consistently with the rest of the system.\n  const resolvedArgs: Record<string, unknown> = {};\n  if (check.args) {\n    for (const [key, argValue] of Object.entries(check.args)) {\n      resolvedArgs[key] = resolvePropValue(argValue, { stateModel });\n    }\n  }\n\n  // Find the validation function\n  const validationFn =\n    builtInValidationFunctions[check.type] ?? customFunctions?.[check.type];\n\n  if (!validationFn) {\n    console.warn(`Unknown validation function: ${check.type}`);\n    return {\n      type: check.type,\n      valid: true, // Don't fail on unknown functions\n      message: check.message,\n    };\n  }\n\n  const valid = validationFn(value, resolvedArgs);\n\n  return {\n    type: check.type,\n    valid,\n    message: check.message,\n  };\n}\n\n/**\n * Run all validation checks for a field\n */\nexport function runValidation(\n  config: ValidationConfig,\n  ctx: ValidationContext,\n): ValidationResult {\n  const checks: ValidationCheckResult[] = [];\n  const errors: string[] = [];\n\n  // Check if validation is enabled\n  if (config.enabled) {\n    const enabled = evaluateVisibility(config.enabled, {\n      stateModel: ctx.stateModel,\n    });\n    if (!enabled) {\n      return { valid: true, errors: [], checks: [] };\n    }\n  }\n\n  // Run each check\n  if (config.checks) {\n    for (const check of config.checks) {\n      const result = runValidationCheck(check, ctx);\n      checks.push(result);\n      if (!result.valid) {\n        errors.push(result.message);\n      }\n    }\n  }\n\n  return {\n    valid: errors.length === 0,\n    errors,\n    checks,\n  };\n}\n\n/**\n * Helper to create validation checks\n */\nexport const check = {\n  required: (message = \"This field is required\"): ValidationCheck => ({\n    type: \"required\",\n    message,\n  }),\n\n  email: (message = \"Invalid email address\"): ValidationCheck => ({\n    type: \"email\",\n    message,\n  }),\n\n  minLength: (min: number, message?: string): ValidationCheck => ({\n    type: \"minLength\",\n    args: { min },\n    message: message ?? `Must be at least ${min} characters`,\n  }),\n\n  maxLength: (max: number, message?: string): ValidationCheck => ({\n    type: \"maxLength\",\n    args: { max },\n    message: message ?? `Must be at most ${max} characters`,\n  }),\n\n  pattern: (pattern: string, message = \"Invalid format\"): ValidationCheck => ({\n    type: \"pattern\",\n    args: { pattern },\n    message,\n  }),\n\n  min: (min: number, message?: string): ValidationCheck => ({\n    type: \"min\",\n    args: { min },\n    message: message ?? `Must be at least ${min}`,\n  }),\n\n  max: (max: number, message?: string): ValidationCheck => ({\n    type: \"max\",\n    args: { max },\n    message: message ?? `Must be at most ${max}`,\n  }),\n\n  url: (message = \"Invalid URL\"): ValidationCheck => ({\n    type: \"url\",\n    message,\n  }),\n\n  numeric: (message = \"Must be a number\"): ValidationCheck => ({\n    type: \"numeric\",\n    message,\n  }),\n\n  matches: (\n    otherPath: string,\n    message = \"Fields must match\",\n  ): ValidationCheck => ({\n    type: \"matches\",\n    args: { other: { $state: otherPath } },\n    message,\n  }),\n\n  equalTo: (\n    otherPath: string,\n    message = \"Fields must match\",\n  ): ValidationCheck => ({\n    type: \"equalTo\",\n    args: { other: { $state: otherPath } },\n    message,\n  }),\n\n  lessThan: (otherPath: string, message?: string): ValidationCheck => ({\n    type: \"lessThan\",\n    args: { other: { $state: otherPath } },\n    message: message ?? \"Must be less than the compared field\",\n  }),\n\n  greaterThan: (otherPath: string, message?: string): ValidationCheck => ({\n    type: \"greaterThan\",\n    args: { other: { $state: otherPath } },\n    message: message ?? \"Must be greater than the compared field\",\n  }),\n\n  requiredIf: (\n    fieldPath: string,\n    message = \"This field is required\",\n  ): ValidationCheck => ({\n    type: \"requiredIf\",\n    args: { field: { $state: fieldPath } },\n    message,\n  }),\n};\n"
  },
  {
    "path": "packages/core/src/visibility.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { evaluateVisibility, visibility } from \"./visibility\";\n\ndescribe(\"evaluateVisibility\", () => {\n  describe(\"undefined / boolean\", () => {\n    it(\"returns true for undefined\", () => {\n      expect(evaluateVisibility(undefined, { stateModel: {} })).toBe(true);\n    });\n\n    it(\"returns true for true\", () => {\n      expect(evaluateVisibility(true, { stateModel: {} })).toBe(true);\n    });\n\n    it(\"returns false for false\", () => {\n      expect(evaluateVisibility(false, { stateModel: {} })).toBe(false);\n    });\n  });\n\n  describe(\"truthiness ($state only)\", () => {\n    it(\"returns true when state path is truthy (boolean)\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/isAdmin\" },\n          { stateModel: { isAdmin: true } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns true when state path is truthy (number)\", () => {\n      expect(\n        evaluateVisibility({ $state: \"/count\" }, { stateModel: { count: 5 } }),\n      ).toBe(true);\n    });\n\n    it(\"returns true when state path is truthy (string)\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/name\" },\n          { stateModel: { name: \"Alice\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when state path is falsy (boolean)\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/isAdmin\" },\n          { stateModel: { isAdmin: false } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns false when state path is falsy (zero)\", () => {\n      expect(\n        evaluateVisibility({ $state: \"/count\" }, { stateModel: { count: 0 } }),\n      ).toBe(false);\n    });\n\n    it(\"returns false when state path is falsy (empty string)\", () => {\n      expect(\n        evaluateVisibility({ $state: \"/name\" }, { stateModel: { name: \"\" } }),\n      ).toBe(false);\n    });\n\n    it(\"returns false when state path is undefined\", () => {\n      expect(\n        evaluateVisibility({ $state: \"/nothing\" }, { stateModel: {} }),\n      ).toBe(false);\n    });\n\n    it(\"returns false for missing path\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/nonexistent\" },\n          { stateModel: { other: true } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"negation ($state + not)\", () => {\n    it(\"returns false when state path is truthy\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/visible\", not: true },\n          { stateModel: { visible: true } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns true when state path is falsy\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/visible\", not: true },\n          { stateModel: { visible: false } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"not inverts an eq condition\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/tab\", eq: \"home\", not: true },\n          { stateModel: { tab: \"home\" } },\n        ),\n      ).toBe(false);\n      expect(\n        evaluateVisibility(\n          { $state: \"/tab\", eq: \"home\", not: true },\n          { stateModel: { tab: \"settings\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"not inverts a gt condition\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gt: 5, not: true },\n          { stateModel: { count: 10 } },\n        ),\n      ).toBe(false);\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gt: 5, not: true },\n          { stateModel: { count: 3 } },\n        ),\n      ).toBe(true);\n    });\n  });\n\n  describe(\"equality ($state + eq)\", () => {\n    it(\"returns true when values match (number)\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", eq: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when values do not match\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", eq: 10 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns true when values match (string)\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/tab\", eq: \"home\" },\n          { stateModel: { tab: \"home\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"supports state-to-state comparison\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/a\", eq: { $state: \"/b\" } },\n          { stateModel: { a: 42, b: 42 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"state-to-state comparison fails when different\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/a\", eq: { $state: \"/b\" } },\n          { stateModel: { a: 1, b: 2 } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"inequality ($state + neq)\", () => {\n    it(\"returns true when values differ\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", neq: 10 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when values are equal\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", neq: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"numeric comparisons\", () => {\n    it(\"gt: returns true when greater\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gt: 3 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"gt: returns false when less\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gt: 3 },\n          { stateModel: { count: 2 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"gt: returns false when equal\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gt: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"gte: returns true when equal\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gte: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"gte: returns true when greater\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gte: 5 },\n          { stateModel: { count: 6 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"gte: returns false when less\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", gte: 5 },\n          { stateModel: { count: 4 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"lt: returns true when less\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lt: 5 },\n          { stateModel: { count: 3 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"lt: returns false when greater\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lt: 5 },\n          { stateModel: { count: 7 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"lt: returns false when equal\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lt: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"lte: returns true when equal\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lte: 5 },\n          { stateModel: { count: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"lte: returns true when less\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lte: 5 },\n          { stateModel: { count: 4 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"lte: returns false when greater\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lte: 5 },\n          { stateModel: { count: 6 } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns false for non-numeric values\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/name\", gt: 5 },\n          { stateModel: { name: \"Alice\" } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"dynamic path references in comparison\", () => {\n    it(\"eq with $state reference on right\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", eq: { $state: \"/limit\" } },\n          { stateModel: { count: 5, limit: 5 } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"lt with $state reference on right\", () => {\n      expect(\n        evaluateVisibility(\n          { $state: \"/count\", lt: { $state: \"/limit\" } },\n          { stateModel: { count: 3, limit: 5 } },\n        ),\n      ).toBe(true);\n    });\n  });\n\n  describe(\"array (implicit AND)\", () => {\n    it(\"returns true when all conditions are true\", () => {\n      expect(\n        evaluateVisibility(\n          [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          { stateModel: { isAdmin: true, tab: \"settings\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when one condition is false\", () => {\n      expect(\n        evaluateVisibility(\n          [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          { stateModel: { isAdmin: false, tab: \"settings\" } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns false when all conditions are false\", () => {\n      expect(\n        evaluateVisibility(\n          [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          { stateModel: { isAdmin: false, tab: \"home\" } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"$and condition (explicit AND)\", () => {\n    it(\"returns true when all children are true\", () => {\n      expect(\n        evaluateVisibility(\n          {\n            $and: [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          },\n          { stateModel: { isAdmin: true, tab: \"settings\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when one child is false\", () => {\n      expect(\n        evaluateVisibility(\n          {\n            $and: [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          },\n          { stateModel: { isAdmin: false, tab: \"settings\" } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"returns false when all children are false\", () => {\n      expect(\n        evaluateVisibility(\n          {\n            $and: [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n          },\n          { stateModel: { isAdmin: false, tab: \"home\" } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"supports nested $or inside $and\", () => {\n      // AND( OR(isAdmin, isModerator), tab=settings )\n      expect(\n        evaluateVisibility(\n          {\n            $and: [\n              { $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }] },\n              { $state: \"/tab\", eq: \"settings\" },\n            ],\n          },\n          {\n            stateModel: {\n              isAdmin: false,\n              isModerator: true,\n              tab: \"settings\",\n            },\n          },\n        ),\n      ).toBe(true);\n      expect(\n        evaluateVisibility(\n          {\n            $and: [\n              { $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }] },\n              { $state: \"/tab\", eq: \"settings\" },\n            ],\n          },\n          {\n            stateModel: {\n              isAdmin: false,\n              isModerator: false,\n              tab: \"settings\",\n            },\n          },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"supports booleans inside $and\", () => {\n      expect(\n        evaluateVisibility(\n          { $and: [true, { $state: \"/ok\" }] },\n          { stateModel: { ok: true } },\n        ),\n      ).toBe(true);\n      expect(\n        evaluateVisibility(\n          { $and: [false, { $state: \"/ok\" }] },\n          { stateModel: { ok: true } },\n        ),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"$or condition\", () => {\n    it(\"returns true when at least one child is true\", () => {\n      expect(\n        evaluateVisibility(\n          { $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }] },\n          { stateModel: { isAdmin: false, isModerator: true } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns true when all children are true\", () => {\n      expect(\n        evaluateVisibility(\n          { $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }] },\n          { stateModel: { isAdmin: true, isModerator: true } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"returns false when all children are false\", () => {\n      expect(\n        evaluateVisibility(\n          { $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }] },\n          { stateModel: { isAdmin: false, isModerator: false } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"supports nested arrays (AND inside OR)\", () => {\n      // OR( AND(isAdmin, tab=settings), isSuperUser )\n      expect(\n        evaluateVisibility(\n          {\n            $or: [\n              [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n              { $state: \"/isSuperUser\" },\n            ],\n          },\n          {\n            stateModel: { isAdmin: true, tab: \"settings\", isSuperUser: false },\n          },\n        ),\n      ).toBe(true);\n      expect(\n        evaluateVisibility(\n          {\n            $or: [\n              [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"settings\" }],\n              { $state: \"/isSuperUser\" },\n            ],\n          },\n          {\n            stateModel: { isAdmin: false, tab: \"settings\", isSuperUser: false },\n          },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"supports booleans inside $or\", () => {\n      expect(\n        evaluateVisibility(\n          { $or: [false, { $state: \"/ok\" }] },\n          { stateModel: { ok: true } },\n        ),\n      ).toBe(true);\n      expect(\n        evaluateVisibility({ $or: [false, false] }, { stateModel: {} }),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"$item conditions\", () => {\n    it(\"$item truthiness check\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"active\" },\n          { stateModel: {}, repeatItem: { active: true } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$item falsy check\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"active\" },\n          { stateModel: {}, repeatItem: { active: false } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"$item equality check\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"status\", eq: \"done\" },\n          { stateModel: {}, repeatItem: { status: \"done\" } },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$item equality check fails\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"status\", eq: \"done\" },\n          { stateModel: {}, repeatItem: { status: \"pending\" } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"$item root reference\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"\", eq: \"hello\" },\n          { stateModel: {}, repeatItem: \"hello\" },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$item with not\", () => {\n      expect(\n        evaluateVisibility(\n          { $item: \"active\", not: true },\n          { stateModel: {}, repeatItem: { active: true } },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"$item returns false when no repeat scope\", () => {\n      expect(evaluateVisibility({ $item: \"x\" }, { stateModel: {} })).toBe(\n        false,\n      );\n    });\n  });\n\n  describe(\"$index conditions\", () => {\n    it(\"$index equality check\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true, eq: 0 },\n          { stateModel: {}, repeatIndex: 0 },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$index equality check fails\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true, eq: 0 },\n          { stateModel: {}, repeatIndex: 1 },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"$index gt check\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true, gt: 2 },\n          { stateModel: {}, repeatIndex: 5 },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$index truthiness\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true },\n          { stateModel: {}, repeatIndex: 3 },\n        ),\n      ).toBe(true);\n    });\n\n    it(\"$index zero is falsy\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true },\n          { stateModel: {}, repeatIndex: 0 },\n        ),\n      ).toBe(false);\n    });\n\n    it(\"$index with not\", () => {\n      expect(\n        evaluateVisibility(\n          { $index: true, eq: 0, not: true },\n          { stateModel: {}, repeatIndex: 1 },\n        ),\n      ).toBe(true);\n    });\n  });\n});\n\ndescribe(\"visibility helper\", () => {\n  it(\"always is true\", () => {\n    expect(visibility.always).toBe(true);\n  });\n\n  it(\"never is false\", () => {\n    expect(visibility.never).toBe(false);\n  });\n\n  it(\"when creates a $state condition\", () => {\n    expect(visibility.when(\"/user/isAdmin\")).toEqual({\n      $state: \"/user/isAdmin\",\n    });\n  });\n\n  it(\"unless creates a negated $state condition\", () => {\n    expect(visibility.unless(\"/form/hasErrors\")).toEqual({\n      $state: \"/form/hasErrors\",\n      not: true,\n    });\n  });\n\n  it(\"eq creates an equality condition\", () => {\n    expect(visibility.eq(\"/tab\", \"home\")).toEqual({\n      $state: \"/tab\",\n      eq: \"home\",\n    });\n  });\n\n  it(\"neq creates an inequality condition\", () => {\n    expect(visibility.neq(\"/role\", \"guest\")).toEqual({\n      $state: \"/role\",\n      neq: \"guest\",\n    });\n  });\n\n  it(\"gt creates a greater-than condition\", () => {\n    expect(visibility.gt(\"/count\", 5)).toEqual({\n      $state: \"/count\",\n      gt: 5,\n    });\n  });\n\n  it(\"gte creates a gte condition\", () => {\n    expect(visibility.gte(\"/count\", 5)).toEqual({\n      $state: \"/count\",\n      gte: 5,\n    });\n  });\n\n  it(\"lt creates a less-than condition\", () => {\n    expect(visibility.lt(\"/count\", 5)).toEqual({\n      $state: \"/count\",\n      lt: 5,\n    });\n  });\n\n  it(\"lte creates a lte condition\", () => {\n    expect(visibility.lte(\"/count\", 5)).toEqual({\n      $state: \"/count\",\n      lte: 5,\n    });\n  });\n\n  it(\"and returns an $and wrapper\", () => {\n    const result = visibility.and(\n      visibility.when(\"/isAdmin\"),\n      visibility.eq(\"/tab\", \"home\"),\n    );\n    expect(result).toEqual({\n      $and: [{ $state: \"/isAdmin\" }, { $state: \"/tab\", eq: \"home\" }],\n    });\n  });\n\n  it(\"or returns an $or wrapper\", () => {\n    const result = visibility.or(\n      visibility.when(\"/isAdmin\"),\n      visibility.when(\"/isModerator\"),\n    );\n    expect(result).toEqual({\n      $or: [{ $state: \"/isAdmin\" }, { $state: \"/isModerator\" }],\n    });\n  });\n});\n"
  },
  {
    "path": "packages/core/src/visibility.ts",
    "content": "import { z } from \"zod\";\nimport type {\n  VisibilityCondition,\n  StateCondition,\n  ItemCondition,\n  IndexCondition,\n  SingleCondition,\n  AndCondition,\n  OrCondition,\n  StateModel,\n} from \"./types\";\nimport { getByPath } from \"./types\";\n\n// =============================================================================\n// Schemas\n// =============================================================================\n\n/**\n * Schema for a single state condition.\n */\nconst numericOrStateRef = z.union([\n  z.number(),\n  z.object({ $state: z.string() }),\n]);\n\nconst comparisonOps = {\n  eq: z.unknown().optional(),\n  neq: z.unknown().optional(),\n  gt: numericOrStateRef.optional(),\n  gte: numericOrStateRef.optional(),\n  lt: numericOrStateRef.optional(),\n  lte: numericOrStateRef.optional(),\n  not: z.literal(true).optional(),\n};\n\nconst StateConditionSchema = z.object({\n  $state: z.string(),\n  ...comparisonOps,\n});\n\nconst ItemConditionSchema = z.object({\n  $item: z.string(),\n  ...comparisonOps,\n});\n\nconst IndexConditionSchema = z.object({\n  $index: z.literal(true),\n  ...comparisonOps,\n});\n\nconst SingleConditionSchema = z.union([\n  StateConditionSchema,\n  ItemConditionSchema,\n  IndexConditionSchema,\n]);\n\n/**\n * Visibility condition schema.\n *\n * Lazy because `OrCondition` can recursively contain `VisibilityCondition`.\n */\nexport const VisibilityConditionSchema: z.ZodType<VisibilityCondition> = z.lazy(\n  () =>\n    z.union([\n      z.boolean(),\n      SingleConditionSchema,\n      z.array(SingleConditionSchema),\n      z.object({ $and: z.array(VisibilityConditionSchema) }),\n      z.object({ $or: z.array(VisibilityConditionSchema) }),\n    ]),\n);\n\n// =============================================================================\n// Context\n// =============================================================================\n\n/**\n * Context for evaluating visibility conditions.\n *\n * `repeatItem` and `repeatIndex` are only present inside a `repeat` scope\n * and enable `$item` / `$index` conditions.\n */\nexport interface VisibilityContext {\n  stateModel: StateModel;\n  /** The current repeat item (set inside a repeat scope). */\n  repeatItem?: unknown;\n  /** The current repeat array index (set inside a repeat scope). */\n  repeatIndex?: number;\n}\n\n// =============================================================================\n// Evaluation\n// =============================================================================\n\n/**\n * Resolve a comparison value. If it's a `{ $state }` reference, look it up;\n * otherwise return the literal.\n */\nfunction resolveComparisonValue(\n  value: unknown,\n  ctx: VisibilityContext,\n): unknown {\n  if (typeof value === \"object\" && value !== null) {\n    if (\n      \"$state\" in value &&\n      typeof (value as Record<string, unknown>).$state === \"string\"\n    ) {\n      return getByPath(ctx.stateModel, (value as { $state: string }).$state);\n    }\n  }\n  return value;\n}\n\n/**\n * Type guards for condition sources.\n */\nfunction isItemCondition(cond: SingleCondition): cond is ItemCondition {\n  return \"$item\" in cond;\n}\n\nfunction isIndexCondition(cond: SingleCondition): cond is IndexCondition {\n  return \"$index\" in cond;\n}\n\n/**\n * Resolve the left-hand-side value of a condition based on its source.\n */\nfunction resolveConditionValue(\n  cond: SingleCondition,\n  ctx: VisibilityContext,\n): unknown {\n  if (isIndexCondition(cond)) {\n    return ctx.repeatIndex;\n  }\n  if (isItemCondition(cond)) {\n    if (ctx.repeatItem === undefined) return undefined;\n    return cond.$item === \"\"\n      ? ctx.repeatItem\n      : getByPath(ctx.repeatItem, cond.$item);\n  }\n  // StateCondition\n  return getByPath(ctx.stateModel, (cond as StateCondition).$state);\n}\n\n/**\n * Evaluate a single condition against the context.\n *\n * When `not` is `true`, the final result is inverted — this applies to\n * whichever operator is present (or to the truthiness check if no operator\n * is given).  For example:\n * - `{ $state: \"/x\", not: true }` → `!Boolean(value)`\n * - `{ $state: \"/x\", gt: 5, not: true }` → `!(value > 5)`\n */\nfunction evaluateCondition(\n  cond: SingleCondition,\n  ctx: VisibilityContext,\n): boolean {\n  const value = resolveConditionValue(cond, ctx);\n  let result: boolean;\n\n  // Equality\n  if (cond.eq !== undefined) {\n    const rhs = resolveComparisonValue(cond.eq, ctx);\n    result = value === rhs;\n  }\n  // Inequality\n  else if (cond.neq !== undefined) {\n    const rhs = resolveComparisonValue(cond.neq, ctx);\n    result = value !== rhs;\n  }\n  // Greater than\n  else if (cond.gt !== undefined) {\n    const rhs = resolveComparisonValue(cond.gt, ctx);\n    result =\n      typeof value === \"number\" && typeof rhs === \"number\"\n        ? value > rhs\n        : false;\n  }\n  // Greater than or equal\n  else if (cond.gte !== undefined) {\n    const rhs = resolveComparisonValue(cond.gte, ctx);\n    result =\n      typeof value === \"number\" && typeof rhs === \"number\"\n        ? value >= rhs\n        : false;\n  }\n  // Less than\n  else if (cond.lt !== undefined) {\n    const rhs = resolveComparisonValue(cond.lt, ctx);\n    result =\n      typeof value === \"number\" && typeof rhs === \"number\"\n        ? value < rhs\n        : false;\n  }\n  // Less than or equal\n  else if (cond.lte !== undefined) {\n    const rhs = resolveComparisonValue(cond.lte, ctx);\n    result =\n      typeof value === \"number\" && typeof rhs === \"number\"\n        ? value <= rhs\n        : false;\n  }\n  // Truthiness (no operator)\n  else {\n    result = Boolean(value);\n  }\n\n  // `not` inverts the result of any condition\n  return cond.not === true ? !result : result;\n}\n\n/**\n * Type guard for AndCondition\n */\nfunction isAndCondition(\n  condition: VisibilityCondition,\n): condition is AndCondition {\n  return (\n    typeof condition === \"object\" &&\n    condition !== null &&\n    !Array.isArray(condition) &&\n    \"$and\" in condition\n  );\n}\n\n/**\n * Type guard for OrCondition\n */\nfunction isOrCondition(\n  condition: VisibilityCondition,\n): condition is OrCondition {\n  return (\n    typeof condition === \"object\" &&\n    condition !== null &&\n    !Array.isArray(condition) &&\n    \"$or\" in condition\n  );\n}\n\n/**\n * Evaluate a visibility condition.\n *\n * - `undefined` → visible\n * - `boolean` → that value\n * - `SingleCondition` → evaluate single condition\n * - `SingleCondition[]` → implicit AND (all must be true)\n * - `AndCondition` → `{ $and: [...] }`, explicit AND\n * - `OrCondition` → `{ $or: [...] }`, at least one must be true\n */\nexport function evaluateVisibility(\n  condition: VisibilityCondition | undefined,\n  ctx: VisibilityContext,\n): boolean {\n  // No condition = visible\n  if (condition === undefined) {\n    return true;\n  }\n\n  // Boolean literal\n  if (typeof condition === \"boolean\") {\n    return condition;\n  }\n\n  // Array = implicit AND\n  if (Array.isArray(condition)) {\n    return condition.every((c) => evaluateCondition(c, ctx));\n  }\n\n  // Explicit AND condition\n  if (isAndCondition(condition)) {\n    return condition.$and.every((child) => evaluateVisibility(child, ctx));\n  }\n\n  // OR condition\n  if (isOrCondition(condition)) {\n    return condition.$or.some((child) => evaluateVisibility(child, ctx));\n  }\n\n  // Single condition\n  return evaluateCondition(condition, ctx);\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\n/**\n * Helper to create visibility conditions.\n */\nexport const visibility = {\n  /** Always visible */\n  always: true as const,\n\n  /** Never visible */\n  never: false as const,\n\n  /** Visible when state path is truthy */\n  when: (path: string): StateCondition => ({ $state: path }),\n\n  /** Visible when state path is falsy */\n  unless: (path: string): StateCondition => ({ $state: path, not: true }),\n\n  /** Equality check */\n  eq: (path: string, value: unknown): StateCondition => ({\n    $state: path,\n    eq: value,\n  }),\n\n  /** Not equal check */\n  neq: (path: string, value: unknown): StateCondition => ({\n    $state: path,\n    neq: value,\n  }),\n\n  /** Greater than */\n  gt: (path: string, value: number | { $state: string }): StateCondition => ({\n    $state: path,\n    gt: value,\n  }),\n\n  /** Greater than or equal */\n  gte: (path: string, value: number | { $state: string }): StateCondition => ({\n    $state: path,\n    gte: value,\n  }),\n\n  /** Less than */\n  lt: (path: string, value: number | { $state: string }): StateCondition => ({\n    $state: path,\n    lt: value,\n  }),\n\n  /** Less than or equal */\n  lte: (path: string, value: number | { $state: string }): StateCondition => ({\n    $state: path,\n    lte: value,\n  }),\n\n  /** AND multiple conditions */\n  and: (...conditions: VisibilityCondition[]): AndCondition => ({\n    $and: conditions,\n  }),\n\n  /** OR multiple conditions */\n  or: (...conditions: VisibilityCondition[]): OrCondition => ({\n    $or: conditions,\n  }),\n};\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/core/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/store-utils.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"zod\"],\n});\n"
  },
  {
    "path": "packages/eslint-config/README.md",
    "content": "# `@turbo/eslint-config`\n\nCollection of internal eslint configurations.\n"
  },
  {
    "path": "packages/eslint-config/base.js",
    "content": "import js from \"@eslint/js\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\nimport turboPlugin from \"eslint-plugin-turbo\";\nimport tseslint from \"typescript-eslint\";\nimport onlyWarn from \"eslint-plugin-only-warn\";\n\n/**\n * A shared ESLint configuration for the repository.\n *\n * @type {import(\"eslint\").Linter.Config[]}\n * */\nexport const config = [\n  js.configs.recommended,\n  eslintConfigPrettier,\n  ...tseslint.configs.recommended,\n  {\n    plugins: {\n      turbo: turboPlugin,\n    },\n    rules: {\n      \"turbo/no-undeclared-env-vars\": \"warn\",\n    },\n  },\n  {\n    plugins: {\n      onlyWarn,\n    },\n  },\n  {\n    ignores: [\"dist/**\"],\n  },\n];\n"
  },
  {
    "path": "packages/eslint-config/next.js",
    "content": "import js from \"@eslint/js\";\nimport { globalIgnores } from \"eslint/config\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\nimport tseslint from \"typescript-eslint\";\nimport pluginReactHooks from \"eslint-plugin-react-hooks\";\nimport pluginReact from \"eslint-plugin-react\";\nimport globals from \"globals\";\nimport pluginNext from \"@next/eslint-plugin-next\";\nimport { config as baseConfig } from \"./base.js\";\n\n/**\n * A custom ESLint configuration for libraries that use Next.js.\n *\n * @type {import(\"eslint\").Linter.Config[]}\n * */\nexport const nextJsConfig = [\n  ...baseConfig,\n  js.configs.recommended,\n  eslintConfigPrettier,\n  ...tseslint.configs.recommended,\n  globalIgnores([\n    // Default ignores of eslint-config-next:\n    \".next/**\",\n    \"out/**\",\n    \"build/**\",\n    \"next-env.d.ts\",\n  ]),\n  {\n    ...pluginReact.configs.flat.recommended,\n    languageOptions: {\n      ...pluginReact.configs.flat.recommended.languageOptions,\n      globals: {\n        ...globals.serviceworker,\n      },\n    },\n  },\n  {\n    plugins: {\n      \"@next/next\": pluginNext,\n    },\n    rules: {\n      ...pluginNext.configs.recommended.rules,\n      ...pluginNext.configs[\"core-web-vitals\"].rules,\n    },\n  },\n  {\n    plugins: {\n      \"react-hooks\": pluginReactHooks,\n    },\n    settings: { react: { version: \"detect\" } },\n    rules: {\n      ...pluginReactHooks.configs.recommended.rules,\n      // React scope no longer necessary with new JSX transform.\n      \"react/react-in-jsx-scope\": \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "packages/eslint-config/package.json",
    "content": "{\n  \"name\": \"@internal/eslint-config\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"exports\": {\n    \"./base\": \"./base.js\",\n    \"./next-js\": \"./next.js\",\n    \"./react-internal\": \"./react-internal.js\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.39.1\",\n    \"@next/eslint-plugin-next\": \"^15.5.0\",\n    \"eslint\": \"^9.39.1\",\n    \"eslint-config-prettier\": \"^10.1.1\",\n    \"eslint-plugin-only-warn\": \"^1.1.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"eslint-plugin-turbo\": \"^2.7.1\",\n    \"globals\": \"^16.5.0\",\n    \"typescript\": \"^5.9.2\",\n    \"typescript-eslint\": \"^8.50.0\"\n  }\n}\n"
  },
  {
    "path": "packages/eslint-config/react-internal.js",
    "content": "import js from \"@eslint/js\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\nimport tseslint from \"typescript-eslint\";\nimport pluginReactHooks from \"eslint-plugin-react-hooks\";\nimport pluginReact from \"eslint-plugin-react\";\nimport globals from \"globals\";\nimport { config as baseConfig } from \"./base.js\";\n\n/**\n * A custom ESLint configuration for libraries that use React.\n *\n * @type {import(\"eslint\").Linter.Config[]} */\nexport const config = [\n  ...baseConfig,\n  js.configs.recommended,\n  eslintConfigPrettier,\n  ...tseslint.configs.recommended,\n  pluginReact.configs.flat.recommended,\n  {\n    languageOptions: {\n      ...pluginReact.configs.flat.recommended.languageOptions,\n      globals: {\n        ...globals.serviceworker,\n        ...globals.browser,\n      },\n    },\n  },\n  {\n    plugins: {\n      \"react-hooks\": pluginReactHooks,\n    },\n    settings: { react: { version: \"detect\" } },\n    rules: {\n      ...pluginReactHooks.configs.recommended.rules,\n      // React scope no longer necessary with new JSX transform.\n      \"react/react-in-jsx-scope\": \"off\",\n    },\n  },\n];\n"
  },
  {
    "path": "packages/image/CHANGELOG.md",
    "content": "# @json-render/image\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Minor Changes\n\n- 3f1e71e: Image renderer: generate SVG and PNG from JSON specs.\n\n  ### New: `@json-render/image` Package\n\n  Server-side image renderer powered by Satori. Turns the same `{ root, elements }` spec format into SVG or PNG output for OG images, social cards, and banners.\n  - `renderToSvg(spec, options)` — render spec to SVG string\n  - `renderToPng(spec, options)` — render spec to PNG buffer (requires `@resvg/resvg-js`)\n  - 9 standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer\n  - `standardComponentDefinitions` catalog for AI prompt generation\n  - Server-safe import path: `@json-render/image/server`\n  - Sub-path exports: `/render`, `/catalog`, `/server`\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n"
  },
  {
    "path": "packages/image/README.md",
    "content": "# @json-render/image\n\nImage renderer for `@json-render/core`. Generate SVG and PNG images from JSON specs using [Satori](https://github.com/vercel/satori).\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/image\n```\n\nFor PNG output, also install the optional peer dependency:\n\n```bash\nnpm install @resvg/resvg-js\n```\n\n## Quick Start\n\n### Render a spec to SVG\n\n```typescript\nimport { renderToSvg } 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\", \"subtitle\"],\n    },\n    heading: {\n      type: \"Heading\",\n      props: { text: \"Hello World\", level: \"h1\", color: \"#ffffff\" },\n      children: [],\n    },\n    subtitle: {\n      type: \"Text\",\n      props: { text: \"Generated from JSON\", fontSize: 24, color: \"#a0a0b0\" },\n      children: [],\n    },\n  },\n};\n\nconst svg = await renderToSvg(spec, {\n  fonts: [\n    {\n      name: \"Inter\",\n      data: await fetch(\"https://example.com/Inter-Regular.ttf\").then((r) =>\n        r.arrayBuffer()\n      ),\n      weight: 400,\n      style: \"normal\",\n    },\n  ],\n});\n```\n\n### Render to PNG\n\n```typescript\nimport { renderToPng } from \"@json-render/image/render\";\n\nconst png = await renderToPng(spec, {\n  fonts: [\n    {\n      name: \"Inter\",\n      data: await readFile(\"./Inter-Regular.ttf\"),\n      weight: 400,\n      style: \"normal\",\n    },\n  ],\n});\n\n// Write to file\nawait writeFile(\"output.png\", png);\n```\n\n### With a custom catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, renderToSvg } from \"@json-render/image\";\nimport { standardComponentDefinitions } from \"@json-render/image/catalog\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().nullable(),\n      }),\n      slots: [],\n      description: \"A colored badge label\",\n    },\n  },\n});\n```\n\n## Standard Components\n\n### Root\n\n| Component | Description |\n|-----------|-------------|\n| `Frame` | Root image container. Defines width, height, and background. Must be the root element. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Box` | Generic container with padding, margin, background, border, and flex alignment. |\n| `Row` | Horizontal flex layout with gap, align, justify. |\n| `Column` | Vertical flex layout with gap, align, justify. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | h1-h4 heading text with color and alignment. |\n| `Text` | Body text with fontSize, color, weight, style, and alignment. |\n| `Image` | Image from a URL with width, height, and borderRadius. |\n\n### Decorative\n\n| Component | Description |\n|-----------|-------------|\n| `Divider` | Horizontal line separator. |\n| `Spacer` | Empty vertical space. |\n\n## Server-Side APIs\n\n```typescript\nimport { renderToSvg, renderToPng } from \"@json-render/image/render\";\n\n// Render to an SVG string\nconst svg = await renderToSvg(spec, { fonts });\n\n// Render to a PNG buffer (requires @resvg/resvg-js)\nconst png = await renderToPng(spec, { fonts });\n```\n\n### Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `fonts` | `SatoriOptions['fonts']` | `[]` | Font data for text rendering |\n| `width` | `number` | Frame prop | Override image width |\n| `height` | `number` | Frame prop | Override image height |\n| `registry` | `Record<string, Component>` | `{}` | Custom component overrides |\n| `includeStandard` | `boolean` | `true` | Include standard components |\n| `state` | `Record<string, unknown>` | `{}` | Initial state values |\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React or Satori:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/image/server\";\n```\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/image/package.json",
    "content": "{\n  \"name\": \"@json-render/image\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.\",\n  \"keywords\": [\n    \"json\",\n    \"image\",\n    \"svg\",\n    \"png\",\n    \"og\",\n    \"satori\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/image\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.mjs\",\n      \"require\": \"./dist/server.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    },\n    \"./render\": {\n      \"types\": \"./dist/render.d.ts\",\n      \"import\": \"./dist/render.mjs\",\n      \"require\": \"./dist/render.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"satori\": \"^0.19.2\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.5.1\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"@resvg/resvg-js\": \">=2.0.0\",\n    \"react\": \"^18.0.0 || ^19.0.0\",\n    \"zod\": \"^4.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@resvg/resvg-js\": {\n      \"optional\": true\n    }\n  }\n}\n"
  },
  {
    "path": "packages/image/src/catalog-types.ts",
    "content": "import type {\n  Catalog,\n  InferCatalogComponents,\n  InferComponentProps,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> {\n  props: InferComponentProps<C, K>;\n  children?: React.ReactNode;\n  emit: (event: string) => void;\n}\n\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => React.ReactNode;\n\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n"
  },
  {
    "path": "packages/image/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Standard component definitions for image catalogs.\n *\n * These define the available image components with their Zod prop schemas.\n * All components render to Satori-compatible JSX (HTML-like elements with\n * inline CSS flexbox styles).\n */\nexport const standardComponentDefinitions = {\n  // ==========================================================================\n  // Root\n  // ==========================================================================\n\n  Frame: {\n    props: z.object({\n      width: z.number(),\n      height: z.number(),\n      backgroundColor: z.string().nullable(),\n      padding: z.number().nullable(),\n      display: z.enum([\"flex\", \"none\"]).nullable(),\n      flexDirection: z.enum([\"row\", \"column\"]).nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Root image container. Defines the output image dimensions and background. Must be the root element.\",\n    example: { width: 1200, height: 630, backgroundColor: \"#ffffff\" },\n  },\n\n  // ==========================================================================\n  // Layout Components\n  // ==========================================================================\n\n  Box: {\n    props: z.object({\n      padding: z.number().nullable(),\n      paddingTop: z.number().nullable(),\n      paddingBottom: z.number().nullable(),\n      paddingLeft: z.number().nullable(),\n      paddingRight: z.number().nullable(),\n      margin: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n      borderWidth: z.number().nullable(),\n      borderColor: z.string().nullable(),\n      borderRadius: z.number().nullable(),\n      flex: z.number().nullable(),\n      width: z.union([z.number(), z.string()]).nullable(),\n      height: z.union([z.number(), z.string()]).nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n      flexDirection: z.enum([\"row\", \"column\"]).nullable(),\n      position: z.enum([\"relative\", \"absolute\"]).nullable(),\n      top: z.number().nullable(),\n      left: z.number().nullable(),\n      right: z.number().nullable(),\n      bottom: z.number().nullable(),\n      overflow: z.enum([\"visible\", \"hidden\"]).nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Generic container with padding, margin, background, border, and flex alignment. Supports absolute positioning.\",\n    example: {\n      padding: 20,\n      backgroundColor: \"#f9f9f9\",\n      borderRadius: 8,\n      alignItems: \"center\",\n    },\n  },\n\n  Row: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n      wrap: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Horizontal flex layout. Use for placing elements side by side.\",\n    example: { gap: 10, alignItems: \"center\" },\n  },\n\n  Column: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Vertical flex layout. Use for stacking elements top to bottom.\",\n    example: { gap: 8, padding: 10 },\n  },\n\n  // ==========================================================================\n  // Content Components\n  // ==========================================================================\n\n  Heading: {\n    props: z.object({\n      text: z.string(),\n      level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n      letterSpacing: z.union([z.number(), z.string()]).nullable(),\n      lineHeight: z.number().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Heading text at various levels. h1 is largest, h4 is smallest.\",\n    example: { text: \"Hello World\", level: \"h1\", color: \"#000000\" },\n  },\n\n  Text: {\n    props: z.object({\n      text: z.string(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n      fontWeight: z.enum([\"normal\", \"bold\"]).nullable(),\n      fontStyle: z.enum([\"normal\", \"italic\"]).nullable(),\n      lineHeight: z.number().nullable(),\n      letterSpacing: z.union([z.number(), z.string()]).nullable(),\n      textDecoration: z.enum([\"none\", \"underline\", \"line-through\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Body text with configurable size, color, weight, and alignment.\",\n    example: { text: \"Some content here.\", fontSize: 16, color: \"#333333\" },\n  },\n\n  Image: {\n    props: z.object({\n      src: z.string(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      borderRadius: z.number().nullable(),\n      objectFit: z.enum([\"contain\", \"cover\", \"fill\", \"none\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Image from a URL. Specify width and/or height to control size. For placeholder images use https://picsum.photos/{width}/{height}?random={n}.\",\n    example: {\n      src: \"https://picsum.photos/400/300?random=1\",\n      width: 400,\n      height: 300,\n    },\n  },\n\n  // ==========================================================================\n  // Decorative Components\n  // ==========================================================================\n\n  Divider: {\n    props: z.object({\n      color: z.string().nullable(),\n      thickness: z.number().nullable(),\n      marginTop: z.number().nullable(),\n      marginBottom: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Horizontal line separator between content sections.\",\n    example: { color: \"#e5e7eb\", thickness: 1 },\n  },\n\n  Spacer: {\n    props: z.object({\n      height: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Empty vertical space between elements.\",\n    example: { height: 20 },\n  },\n};\n\nexport type StandardComponentDefinitions = typeof standardComponentDefinitions;\n\nexport type StandardComponentProps<\n  K extends keyof StandardComponentDefinitions,\n> = StandardComponentDefinitions[K][\"props\"] extends { _output: infer O }\n  ? O\n  : z.output<StandardComponentDefinitions[K][\"props\"]>;\n"
  },
  {
    "path": "packages/image/src/components/index.ts",
    "content": "export { standardComponents } from \"./standard\";\n"
  },
  {
    "path": "packages/image/src/components/standard.tsx",
    "content": "import React from \"react\";\nimport type { ComponentRenderProps, ComponentRegistry } from \"../types\";\nimport type { StandardComponentProps } from \"../catalog\";\n\nconst headingSizes: Record<string, number> = {\n  h1: 48,\n  h2: 36,\n  h3: 28,\n  h4: 22,\n};\n\nconst headingWeights: Record<string, number> = {\n  h1: 700,\n  h2: 700,\n  h3: 600,\n  h4: 600,\n};\n\n/**\n * Satori crashes on explicit `undefined` style values (e.g. `padding: undefined`).\n * Strip them so only defined properties are passed.\n */\nfunction cleanStyle(raw: Record<string, unknown>): React.CSSProperties {\n  const out: Record<string, unknown> = {};\n  for (const k in raw) {\n    if (raw[k] !== undefined && raw[k] !== null) {\n      out[k] = raw[k];\n    }\n  }\n  return out as React.CSSProperties;\n}\n\n// =============================================================================\n// Root\n// =============================================================================\n\nfunction FrameComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Frame\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={cleanStyle({\n        display: p.display ?? \"flex\",\n        flexDirection: p.flexDirection ?? \"column\",\n        width: p.width,\n        height: p.height,\n        backgroundColor: p.backgroundColor ?? \"white\",\n        padding: p.padding,\n        alignItems: p.alignItems,\n        justifyContent: p.justifyContent,\n      })}\n    >\n      {children}\n    </div>\n  );\n}\n\n// =============================================================================\n// Layout Components\n// =============================================================================\n\nfunction BoxComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Box\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={cleanStyle({\n        display: \"flex\",\n        flexDirection: p.flexDirection ?? \"column\",\n        padding: p.padding,\n        paddingTop: p.paddingTop,\n        paddingBottom: p.paddingBottom,\n        paddingLeft: p.paddingLeft,\n        paddingRight: p.paddingRight,\n        margin: p.margin,\n        backgroundColor: p.backgroundColor,\n        borderWidth: p.borderWidth,\n        borderColor: p.borderColor,\n        borderRadius: p.borderRadius,\n        borderStyle: p.borderWidth ? \"solid\" : undefined,\n        flex: p.flex,\n        width: p.width,\n        height: p.height,\n        alignItems: p.alignItems,\n        justifyContent: p.justifyContent,\n        position: p.position,\n        top: p.top,\n        left: p.left,\n        right: p.right,\n        bottom: p.bottom,\n        overflow: p.overflow,\n      })}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction RowComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Row\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={cleanStyle({\n        display: \"flex\",\n        flexDirection: \"row\",\n        gap: p.gap,\n        alignItems: p.alignItems,\n        justifyContent: p.justifyContent,\n        padding: p.padding,\n        flex: p.flex,\n        flexWrap: p.wrap ? \"wrap\" : undefined,\n      })}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction ColumnComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Column\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={cleanStyle({\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: p.gap,\n        alignItems: p.alignItems,\n        justifyContent: p.justifyContent,\n        padding: p.padding,\n        flex: p.flex,\n      })}\n    >\n      {children}\n    </div>\n  );\n}\n\n// =============================================================================\n// Content Components\n// =============================================================================\n\nfunction HeadingComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Heading\">>) {\n  const p = element.props;\n  const level = p.level ?? \"h2\";\n\n  return (\n    <div\n      style={cleanStyle({\n        display: \"flex\",\n        fontSize: headingSizes[level] ?? 36,\n        fontWeight: headingWeights[level] ?? 700,\n        color: p.color ?? \"black\",\n        textAlign: p.align ?? \"left\",\n        letterSpacing: p.letterSpacing,\n        lineHeight: p.lineHeight ?? 1.2,\n      })}\n    >\n      {p.text}\n    </div>\n  );\n}\n\nfunction TextComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Text\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={cleanStyle({\n        display: \"flex\",\n        fontSize: p.fontSize ?? 16,\n        color: p.color ?? \"black\",\n        textAlign: p.align ?? \"left\",\n        fontWeight: p.fontWeight === \"bold\" ? 700 : 400,\n        fontStyle: p.fontStyle ?? \"normal\",\n        lineHeight: p.lineHeight ?? 1.4,\n        letterSpacing: p.letterSpacing,\n        textDecoration: p.textDecoration,\n      })}\n    >\n      {p.text}\n    </div>\n  );\n}\n\nfunction ImageComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Image\">>) {\n  const p = element.props;\n\n  return (\n    <img\n      src={p.src}\n      width={p.width ?? undefined}\n      height={p.height ?? undefined}\n      style={cleanStyle({\n        borderRadius: p.borderRadius,\n        objectFit: p.objectFit ?? \"contain\",\n      })}\n    />\n  );\n}\n\n// =============================================================================\n// Decorative Components\n// =============================================================================\n\nfunction DividerComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Divider\">>) {\n  const p = element.props;\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        width: \"100%\",\n        borderBottom: `${p.thickness ?? 1}px solid ${p.color ?? \"#e5e7eb\"}`,\n        marginTop: p.marginTop ?? 8,\n        marginBottom: p.marginBottom ?? 8,\n      }}\n    />\n  );\n}\n\nfunction SpacerComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Spacer\">>) {\n  const p = element.props;\n\n  return <div style={{ display: \"flex\", height: p.height ?? 20 }} />;\n}\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport const standardComponents: ComponentRegistry = {\n  Frame: FrameComponent,\n  Box: BoxComponent,\n  Row: RowComponent,\n  Column: ColumnComponent,\n  Heading: HeadingComponent,\n  Text: TextComponent,\n  Image: ImageComponent,\n  Divider: DividerComponent,\n  Spacer: SpacerComponent,\n};\n"
  },
  {
    "path": "packages/image/src/index.ts",
    "content": "// Schema\nexport { schema, type ImageSchema, type ImageSpec } from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec } from \"@json-render/core\";\n\n// Catalog-aware types\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n\n// Renderer types\nexport type {\n  ComponentRenderProps,\n  ComponentRenderer,\n  ComponentRegistry,\n} from \"./types\";\n\n// Standard components\nexport { standardComponents } from \"./components\";\n\n// Server-side render functions\nexport { renderToSvg, renderToPng, type RenderOptions } from \"./render\";\n\n// Catalog definitions\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/image/src/render.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport type { Spec } from \"@json-render/core\";\nimport { renderToSvg, renderToPng } from \"./render\";\n\nconst minimalSpec: Spec = {\n  root: \"frame\",\n  elements: {\n    frame: {\n      type: \"Frame\",\n      props: {\n        width: 400,\n        height: 200,\n        backgroundColor: \"#ffffff\",\n      },\n      children: [\"box\"],\n    },\n    box: {\n      type: \"Box\",\n      props: {\n        width: 100,\n        height: 50,\n        backgroundColor: \"#ff0000\",\n        borderRadius: 8,\n      },\n      children: [],\n    },\n  },\n};\n\ndescribe(\"renderToSvg\", () => {\n  it(\"produces a valid SVG string\", async () => {\n    const svg = await renderToSvg(minimalSpec);\n    expect(svg).toContain(\"<svg\");\n    expect(svg).toContain(\"</svg>\");\n  });\n\n  it(\"respects width/height from Frame props\", async () => {\n    const svg = await renderToSvg(minimalSpec);\n    expect(svg).toContain('width=\"400\"');\n    expect(svg).toContain('height=\"200\"');\n  });\n\n  it(\"uses explicit width/height options over Frame props\", async () => {\n    const svg = await renderToSvg(minimalSpec, { width: 800, height: 600 });\n    expect(svg).toContain('width=\"800\"');\n    expect(svg).toContain('height=\"600\"');\n  });\n\n  it(\"falls back to defaults when Frame has no dimensions\", async () => {\n    const spec: Spec = {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: { backgroundColor: \"#000\" },\n          children: [],\n        },\n      },\n    };\n    const svg = await renderToSvg(spec);\n    expect(svg).toContain('width=\"1200\"');\n    expect(svg).toContain('height=\"630\"');\n  });\n\n  it(\"gracefully skips unknown component types\", async () => {\n    const spec: Spec = {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: { width: 400, height: 200 },\n          children: [\"unknown\"],\n        },\n        unknown: {\n          type: \"NonExistent\",\n          props: {},\n          children: [],\n        },\n      },\n    };\n    const svg = await renderToSvg(spec);\n    expect(svg).toContain(\"<svg\");\n  });\n\n  it(\"renders without standard components when includeStandard is false\", async () => {\n    const spec: Spec = {\n      root: \"frame\",\n      elements: {\n        frame: {\n          type: \"Frame\",\n          props: { width: 400, height: 200 },\n          children: [],\n        },\n      },\n    };\n    const svg = await renderToSvg(spec, { includeStandard: false });\n    expect(svg).toContain(\"<svg\");\n  });\n});\n\ndescribe(\"renderToPng\", () => {\n  it(\"produces a buffer with content\", async () => {\n    const png = await renderToPng(minimalSpec);\n    expect(png.length).toBeGreaterThan(0);\n    expect(png.byteLength).toBeGreaterThan(0);\n  });\n\n  it(\"starts with PNG magic bytes\", async () => {\n    const png = await renderToPng(minimalSpec);\n    expect(png[0]).toBe(0x89);\n    expect(png[1]).toBe(0x50); // P\n    expect(png[2]).toBe(0x4e); // N\n    expect(png[3]).toBe(0x47); // G\n  });\n});\n"
  },
  {
    "path": "packages/image/src/render.tsx",
    "content": "import React from \"react\";\nimport satori, { type SatoriOptions } from \"satori\";\nimport type { Spec, UIElement } from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n} from \"@json-render/core\";\nimport { standardComponents } from \"./components/standard\";\nimport type { ComponentRegistry } from \"./types\";\n\nexport { standardComponents };\n\nexport interface RenderOptions {\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;\n  state?: Record<string, unknown>;\n  fonts?: SatoriOptions[\"fonts\"];\n  /** Override the Frame width. When omitted, uses the Frame component's width prop. */\n  width?: number;\n  /** Override the Frame height. When omitted, uses the Frame component's height prop. */\n  height?: number;\n}\n\nconst noopEmit = () => {};\n\nfunction renderElement(\n  elementKey: string,\n  spec: Spec,\n  registry: ComponentRegistry,\n  stateModel: Record<string, unknown>,\n  repeatItem?: unknown,\n  repeatIndex?: number,\n  repeatBasePath?: string,\n): React.ReactElement | null {\n  const element = spec.elements[elementKey];\n  if (!element) return null;\n\n  const ctx: PropResolutionContext = {\n    stateModel,\n    repeatItem,\n    repeatIndex,\n    repeatBasePath,\n  };\n\n  if (element.visible !== undefined) {\n    if (!evaluateVisibility(element.visible, ctx)) {\n      return null;\n    }\n  }\n\n  const resolvedProps = resolveElementProps(\n    element.props as Record<string, unknown>,\n    ctx,\n  );\n  const resolvedElement: UIElement = { ...element, props: resolvedProps };\n\n  const Component = registry[resolvedElement.type];\n  if (!Component) return null;\n\n  if (resolvedElement.repeat) {\n    const items =\n      (getByPath(stateModel, resolvedElement.repeat.statePath) as\n        | unknown[]\n        | undefined) ?? [];\n\n    const fragments = items.map((item, index) => {\n      const key =\n        resolvedElement.repeat!.key && typeof item === \"object\" && item !== null\n          ? String(\n              (item as Record<string, unknown>)[resolvedElement.repeat!.key!] ??\n                index,\n            )\n          : String(index);\n\n      const childPath = `${resolvedElement.repeat!.statePath}/${index}`;\n      const children = resolvedElement.children?.map((childKey) =>\n        renderElement(\n          childKey,\n          spec,\n          registry,\n          stateModel,\n          item,\n          index,\n          childPath,\n        ),\n      );\n\n      return (\n        <Component key={key} element={resolvedElement} emit={noopEmit}>\n          {children}\n        </Component>\n      );\n    });\n\n    return <>{fragments}</>;\n  }\n\n  const children = resolvedElement.children?.map((childKey) =>\n    renderElement(\n      childKey,\n      spec,\n      registry,\n      stateModel,\n      repeatItem,\n      repeatIndex,\n      repeatBasePath,\n    ),\n  );\n\n  return (\n    <Component key={elementKey} element={resolvedElement} emit={noopEmit}>\n      {children && children.length > 0 ? children : undefined}\n    </Component>\n  );\n}\n\ninterface ImageDimensions {\n  width: number;\n  height: number;\n}\n\nfunction getDimensions(\n  spec: Spec,\n  options: RenderOptions = {},\n): ImageDimensions {\n  if (options.width && options.height) {\n    return { width: options.width, height: options.height };\n  }\n\n  const rootElement = spec.elements[spec.root];\n  const props = rootElement?.props as Record<string, unknown> | undefined;\n\n  return {\n    width: options.width ?? (props?.width as number) ?? 1200,\n    height: options.height ?? (props?.height as number) ?? 630,\n  };\n}\n\nfunction buildTree(\n  spec: Spec,\n  options: RenderOptions = {},\n): React.ReactElement {\n  const {\n    registry: customRegistry,\n    includeStandard = true,\n    state = {},\n  } = options;\n\n  const mergedState: Record<string, unknown> = {\n    ...spec.state,\n    ...state,\n  };\n\n  const registry: ComponentRegistry = {\n    ...(includeStandard ? standardComponents : {}),\n    ...customRegistry,\n  };\n\n  const root = renderElement(spec.root, spec, registry, mergedState);\n  return root ?? <></>;\n}\n\n/**\n * Render a json-render spec to an SVG string.\n *\n * Uses Satori to convert the spec's component tree into SVG.\n * No additional dependencies are needed beyond satori.\n */\nexport async function renderToSvg(\n  spec: Spec,\n  options: RenderOptions = {},\n): Promise<string> {\n  const tree = buildTree(spec, options);\n  const { width, height } = getDimensions(spec, options);\n\n  return satori(tree as React.ReactNode, {\n    width,\n    height,\n    fonts: options.fonts ?? [],\n  });\n}\n\n/**\n * Render a json-render spec to a PNG buffer.\n *\n * Requires `@resvg/resvg-js` to be installed as a peer dependency.\n * The SVG is first generated via Satori, then rasterized to PNG.\n */\nexport async function renderToPng(\n  spec: Spec,\n  options: RenderOptions = {},\n): Promise<Uint8Array> {\n  const svg = await renderToSvg(spec, options);\n\n  let Resvg: typeof import(\"@resvg/resvg-js\").Resvg;\n  try {\n    const mod = await import(\"@resvg/resvg-js\");\n    Resvg = mod.Resvg;\n  } catch {\n    throw new Error(\n      \"@resvg/resvg-js is required for PNG output. Install it with: npm install @resvg/resvg-js\",\n    );\n  }\n\n  const resvg = new Resvg(svg);\n  const pngData = resvg.render();\n  return pngData.asPng();\n}\n"
  },
  {
    "path": "packages/image/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/image\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas\n *\n * Reuses the same { root, elements } spec format as the React and React PDF renderers.\n */\nexport const schema = defineSchema(\n  (s) => ({\n    spec: s.object({\n      root: s.string(),\n      elements: s.record(\n        s.object({\n          type: s.ref(\"catalog.components\"),\n          props: s.propsOf(\"catalog.components\"),\n          children: s.array(s.string()),\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    catalog: s.object({\n      components: s.map({\n        props: s.zod(),\n        slots: s.array(s.string()),\n        description: s.string(),\n        example: s.any(),\n      }),\n    }),\n  }),\n  {\n    defaultRules: [\n      \"The root element MUST be a Frame component. It defines the image dimensions (width, height) and background.\",\n      \"Frame width and height determine the output image size. Common sizes: 1200x630 (OG image), 1080x1080 (social square), 1920x1080 (banner).\",\n      \"Use Row for horizontal layouts and Column for vertical layouts. Both support gap, align, and justify props.\",\n      \"All text content must use Heading or Text components. Raw strings are not supported.\",\n      \"Image src must be a fully qualified URL. For placeholder images, use https://picsum.photos/{width}/{height}?random={n}.\",\n      \"Satori renders a subset of CSS: flexbox layout, borders, backgrounds, text styling. Absolute positioning is supported via position/top/left/right/bottom.\",\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist.\",\n    ],\n  },\n);\n\nexport type ImageSchema = typeof schema;\n\nexport type ImageSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/image/src/server.ts",
    "content": "// Server-safe entry point: schema and catalog definitions only.\n// Does not import React or Satori.\n\nexport { schema, type ImageSchema, type ImageSpec } from \"./schema\";\n\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n\nexport type { Spec } from \"@json-render/core\";\n\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n"
  },
  {
    "path": "packages/image/src/types.ts",
    "content": "import type { ComponentType, ReactNode } from \"react\";\nimport type { UIElement } from \"@json-render/core\";\n\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  element: UIElement<string, P>;\n  children?: ReactNode;\n  emit: (event: string) => void;\n}\n\nexport type ComponentRenderer<P = Record<string, unknown>> = ComponentType<\n  ComponentRenderProps<P>\n>;\n\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n"
  },
  {
    "path": "packages/image/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/image/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/server.ts\", \"src/catalog.ts\", \"src/render.tsx\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"@json-render/core\",\n    \"satori\",\n    \"@resvg/resvg-js\",\n    \"zod\",\n    \"react\",\n    \"react/jsx-runtime\",\n  ],\n});\n"
  },
  {
    "path": "packages/jotai/CHANGELOG.md",
    "content": "# @json-render/jotai\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n"
  },
  {
    "path": "packages/jotai/README.md",
    "content": "# @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```ts\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\nIf 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```ts\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#### Options\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 created internally. Pass your own to share state with `<Provider>`. |\n"
  },
  {
    "path": "packages/jotai/package.json",
    "content": "{\n  \"name\": \"@json-render/jotai\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Jotai adapter for json-render StateStore\",\n  \"keywords\": [\n    \"json-render\",\n    \"jotai\",\n    \"state-management\",\n    \"adapter\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/jotai\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"peerDependencies\": {\n    \"jotai\": \">=2.0.0\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"jotai\": \"^2.18.0\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "packages/jotai/src/index.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { atom } from \"jotai\";\nimport { createStore } from \"jotai/vanilla\";\nimport { jotaiStateStore } from \"./index\";\n\nfunction createTestStore(initial: Record<string, unknown> = {}) {\n  const stateAtom = atom<Record<string, unknown>>(initial);\n  const jStore = createStore();\n  const store = jotaiStateStore({ atom: stateAtom, store: jStore });\n  return { stateAtom, jStore, store };\n}\n\ndescribe(\"jotaiStateStore\", () => {\n  it(\"get/set round-trip\", () => {\n    const { store } = createTestStore({ count: 0 });\n\n    expect(store.get(\"/count\")).toBe(0);\n\n    store.set(\"/count\", 42);\n\n    expect(store.get(\"/count\")).toBe(42);\n    expect(store.getSnapshot().count).toBe(42);\n  });\n\n  it(\"update round-trip with multiple values\", () => {\n    const { store } = createTestStore({});\n\n    store.update({ \"/a\": 1, \"/b\": \"hello\" });\n\n    expect(store.get(\"/a\")).toBe(1);\n    expect(store.get(\"/b\")).toBe(\"hello\");\n    expect(store.getSnapshot()).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"subscribe fires on set\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"subscribe fires on update\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"unsubscribe stops notifications\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    const unsub = store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n    expect(listener).toHaveBeenCalledTimes(1);\n\n    unsub();\n    store.set(\"/x\", 2);\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"getSnapshot immutability -- previous snapshot is not mutated\", () => {\n    const { store } = createTestStore({ user: { name: \"Alice\", age: 30 } });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/user/name\", \"Bob\");\n    const snap2 = store.getSnapshot();\n\n    expect(snap1.user).toEqual({ name: \"Alice\", age: 30 });\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect(snap1.user).not.toBe(snap2.user);\n  });\n\n  it(\"structural sharing -- untouched branches keep references\", () => {\n    const { store } = createTestStore({\n      a: { x: 1 },\n      b: { y: 2 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/a/x\", 99);\n    const snap2 = store.getSnapshot();\n\n    expect(snap2.b).toBe(snap1.b);\n    expect(snap2.a).not.toBe(snap1.a);\n  });\n\n  it(\"getServerSnapshot returns same as getSnapshot\", () => {\n    const { store } = createTestStore({ x: 1 });\n\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n\n    store.set(\"/x\", 2);\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n  });\n\n  it(\"set skips update when value is unchanged\", () => {\n    const { store } = createTestStore({ x: 1 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"update skips update when no values changed\", () => {\n    const { store } = createTestStore({ a: 1, b: 2 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"reads from the shared Jotai store\", () => {\n    const stateAtom = atom<Record<string, unknown>>({ count: 0 });\n    const jStore = createStore();\n    const store = jotaiStateStore({ atom: stateAtom, store: jStore });\n\n    jStore.set(stateAtom, { count: 99 });\n\n    expect(store.get(\"/count\")).toBe(99);\n    expect(store.getSnapshot().count).toBe(99);\n  });\n\n  it(\"creates an internal store when none is provided\", () => {\n    const stateAtom = atom<Record<string, unknown>>({ value: \"hello\" });\n    const store = jotaiStateStore({ atom: stateAtom });\n\n    expect(store.get(\"/value\")).toBe(\"hello\");\n\n    store.set(\"/value\", \"world\");\n    expect(store.get(\"/value\")).toBe(\"world\");\n  });\n});\n"
  },
  {
    "path": "packages/jotai/src/index.ts",
    "content": "import type { StateModel, StateStore } from \"@json-render/core\";\nimport { createStoreAdapter } from \"@json-render/core/store-utils\";\nimport type { WritableAtom } from \"jotai\";\nimport { createStore as createJotaiStore } from \"jotai/vanilla\";\n\nexport type { StateStore } from \"@json-render/core\";\n\ntype JotaiStore = ReturnType<typeof createJotaiStore>;\n\n/**\n * Options for {@link jotaiStateStore}.\n */\nexport interface JotaiStateStoreOptions {\n  /** A writable atom that holds the json-render state model. */\n  atom: WritableAtom<StateModel, [StateModel], void>;\n  /**\n   * The Jotai store instance. Defaults to `createStore()` from `jotai/vanilla`.\n   * Pass your own if you use a `<Provider store={...}>` in your React tree.\n   */\n  store?: JotaiStore;\n}\n\n/**\n * Create a {@link StateStore} backed by a Jotai atom.\n *\n * @example\n * ```ts\n * import { atom } from \"jotai\";\n * import { jotaiStateStore } from \"@json-render/jotai\";\n *\n * const uiAtom = atom<Record<string, unknown>>({ count: 0 });\n *\n * const store = jotaiStateStore({ atom: uiAtom });\n *\n * <StateProvider store={store}>...</StateProvider>\n * ```\n *\n * @example With a shared Jotai store:\n * ```ts\n * import { atom, createStore } from \"jotai\";\n *\n * const jStore = createStore();\n * const uiAtom = atom<Record<string, unknown>>({ count: 0 });\n *\n * const store = jotaiStateStore({ atom: uiAtom, store: jStore });\n *\n * // In React:\n * <JotaiProvider store={jStore}>\n *   <StateProvider store={store}>...</StateProvider>\n * </JotaiProvider>\n * ```\n */\nexport function jotaiStateStore(options: JotaiStateStoreOptions): StateStore {\n  const stateAtom = options.atom;\n  const jStore = options.store ?? createJotaiStore();\n\n  return createStoreAdapter({\n    getSnapshot: () => jStore.get(stateAtom),\n    setSnapshot: (next) => jStore.set(stateAtom, next),\n    subscribe: (listener) => jStore.sub(stateAtom, listener),\n  });\n}\n"
  },
  {
    "path": "packages/jotai/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/jotai/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"@json-render/core\", \"@json-render/core/store-utils\", \"jotai\"],\n});\n"
  },
  {
    "path": "packages/mcp/CHANGELOG.md",
    "content": "# @json-render/mcp\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.\n\n  ### Changed:\n  - **`@json-render/core`** — Renamed generation modes from `\"generate\"` / `\"chat\"` to `\"standalone\"` / `\"inline\"`. The old names still work but emit a deprecation warning.\n\n  ### Fixed:\n  - **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.\n\n  ### Other:\n  - Updated `homepage` URLs across all packages to point to `https://json-render.dev`.\n  - Reorganized skills directory structure for cleaner naming.\n  - Added skills documentation page to the web app.\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Minor Changes\n\n- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.\n\n  ### New:\n  - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.\n  - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.\n  - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.\n\n  ### Fixed:\n  - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n"
  },
  {
    "path": "packages/mcp/README.md",
    "content": "# @json-render/mcp\n\nMCP Apps integration for [json-render](https://github.com/vercel-labs/json-render). Serve json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.\n\n## What are MCP Apps?\n\n[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that lets MCP servers return interactive HTML UIs rendered directly inside chat conversations. Instead of text-only tool responses, users get full interactive interfaces -- dashboards, forms, data visualizations -- embedded inline.\n\n## Installation\n\n```bash\nnpm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk\n```\n\n## Quick Start\n\n### 1. Define your catalog\n\n```ts\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\n\nconst catalog = defineCatalog(schema, {\n  components: { ...shadcnComponentDefinitions },\n  actions: {},\n});\n```\n\n### 2. Create the MCP server\n\n```ts\nimport { createMcpApp } from \"@json-render/mcp\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport fs from \"node:fs\";\n\nconst server = createMcpApp({\n  name: \"My Dashboard\",\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### 3. Build the UI (iframe)\n\nCreate a React app that uses `useJsonRenderApp` from `@json-render/mcp/app`:\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, connected, error } = useJsonRenderApp();\n\n  if (error) return <div>Error: {error.message}</div>;\n  if (!spec) return <div>Waiting for spec...</div>;\n\n  return (\n    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>\n      <Renderer spec={spec} registry={registry} loading={loading} />\n    </JSONUIProvider>\n  );\n}\n```\n\nBundle with Vite + `vite-plugin-singlefile` into a single HTML file, then pass it to `createMcpApp` as the `html` option.\n\n### 4. Connect to a client\n\nAdd to `.cursor/mcp.json` or Claude Desktop config:\n\n```json\n{\n  \"mcpServers\": {\n    \"my-app\": {\n      \"command\": \"node\",\n      \"args\": [\"./server.js\", \"--stdio\"]\n    }\n  }\n}\n```\n\n## API Reference\n\n### Server Side (main export)\n\n#### `createMcpApp(options)`\n\nCreates a fully-configured `McpServer` with a json-render tool and UI resource.\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `name` | `string` | Server name shown in client UIs |\n| `version` | `string` | Server version |\n| `catalog` | `Catalog` | json-render catalog defining available components |\n| `html` | `string` | Bundled HTML for the iframe UI |\n| `tool` | `McpToolOptions` | Optional tool name/title/description overrides |\n\n#### `registerJsonRenderTool(server, options)`\n\nRegister a json-render tool on an existing `McpServer`.\n\n#### `registerJsonRenderResource(server, options)`\n\nRegister a json-render UI resource on an existing `McpServer`.\n\n### Client Side (`@json-render/mcp/app`)\n\n#### `useJsonRenderApp(options?)`\n\nReact hook for the iframe-side app. Connects to the MCP host, receives tool results, and maintains the current json-render spec.\n\nReturns `{ spec, loading, connected, connecting, error, app, callServerTool }`.\n\n#### `buildAppHtml(options)`\n\nGenerate a self-contained HTML string from bundled JS/CSS for use as a UI resource.\n\n## Client Support\n\nMCP Apps are supported by Claude, ChatGPT, VS Code (Copilot), Cursor, Goose, and Postman.\n"
  },
  {
    "path": "packages/mcp/package.json",
    "content": "{\n  \"name\": \"@json-render/mcp\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"mcp\",\n    \"model-context-protocol\",\n    \"mcp-apps\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"claude\",\n    \"chatgpt\",\n    \"cursor\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/mcp\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./app\": {\n      \"types\": \"./dist/app.d.ts\",\n      \"import\": \"./dist/app.mjs\",\n      \"require\": \"./dist/app.js\"\n    },\n    \"./build-app-html\": {\n      \"types\": \"./dist/build-app-html-entry.d.ts\",\n      \"import\": \"./dist/build-app-html-entry.mjs\",\n      \"require\": \"./dist/build-app-html-entry.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@modelcontextprotocol/ext-apps\": \"^1.2.0\",\n    \"@modelcontextprotocol/sdk\": \"^1.27.1\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react\": {\n      \"optional\": true\n    },\n    \"react-dom\": {\n      \"optional\": true\n    }\n  }\n}\n"
  },
  {
    "path": "packages/mcp/src/app.ts",
    "content": "/**\n * Client-side (iframe) utilities for rendering json-render specs\n * inside an MCP App view.\n *\n * This module is intended to run **inside the sandboxed iframe** that\n * MCP hosts render. It connects to the host via the MCP Apps protocol,\n * receives tool results containing json-render specs, and provides\n * React hooks / helpers to render them.\n *\n * @example\n * ```tsx\n * import { useJsonRenderApp } from \"@json-render/mcp/app\";\n * import { Renderer } from \"@json-render/react\";\n *\n * function McpAppView({ registry }) {\n *   const { spec, loading } = useJsonRenderApp();\n *   return <Renderer spec={spec} registry={registry} loading={loading} />;\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nexport { useJsonRenderApp } from \"./use-json-render-app.js\";\nexport type {\n  UseJsonRenderAppOptions,\n  UseJsonRenderAppReturn,\n} from \"./use-json-render-app.js\";\nexport { buildAppHtml } from \"./build-app-html.js\";\nexport type { BuildAppHtmlOptions } from \"./build-app-html.js\";\n"
  },
  {
    "path": "packages/mcp/src/build-app-html-entry.ts",
    "content": "/**\n * Server-side utility for building self-contained MCP App HTML.\n *\n * This entry point does NOT depend on React and is safe to import\n * in Node.js / server environments.\n *\n * @packageDocumentation\n */\n\nexport { buildAppHtml } from \"./build-app-html.js\";\nexport type { BuildAppHtmlOptions } from \"./build-app-html.js\";\n"
  },
  {
    "path": "packages/mcp/src/build-app-html.ts",
    "content": "/**\n * Options for `buildAppHtml`.\n */\nexport interface BuildAppHtmlOptions {\n  /** Title for the HTML page. Defaults to `\"json-render\"`. */\n  title?: string;\n  /**\n   * Inline CSS to inject into the page `<style>` tag.\n   * Use this to include Tailwind output or custom styles.\n   */\n  css?: string;\n  /**\n   * Bundled JavaScript for the app entry point.\n   * This should be the output of a bundler (Vite, esbuild, etc.)\n   * that bundles your React app, registry, and the\n   * `useJsonRenderApp` hook into a single script.\n   */\n  js: string;\n  /**\n   * Additional `<head>` content (meta tags, font links, etc.).\n   */\n  head?: string;\n}\n\n/**\n * Build a self-contained HTML string for an MCP App UI resource.\n *\n * The resulting HTML is designed to be served as a `ui://` resource\n * via `registerJsonRenderResource` or `createMcpApp`.\n *\n * Typically you'd use a bundler (Vite + `vite-plugin-singlefile`, or\n * esbuild) to produce the `js` and `css` strings, then pass them here.\n *\n * @example\n * ```ts\n * import { buildAppHtml } from \"@json-render/mcp/app\";\n * import { readFileSync } from \"node:fs\";\n *\n * const html = buildAppHtml({\n *   title: \"Dashboard\",\n *   js: readFileSync(\"dist/app.js\", \"utf-8\"),\n *   css: readFileSync(\"dist/app.css\", \"utf-8\"),\n * });\n * ```\n */\nexport function buildAppHtml(options: BuildAppHtmlOptions): string {\n  const { title = \"json-render\", css = \"\", js, head = \"\" } = options;\n\n  return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <title>${escapeHtml(title)}</title>\n  ${head}\n  <style>${css}</style>\n</head>\n<body>\n  <div id=\"root\"></div>\n  <script type=\"module\">${js}</script>\n</body>\n</html>`;\n}\n\nfunction escapeHtml(str: string): string {\n  return str\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\");\n}\n"
  },
  {
    "path": "packages/mcp/src/index.ts",
    "content": "export type {\n  CreateMcpAppOptions,\n  McpToolOptions,\n  RegisterToolOptions,\n  RegisterResourceOptions,\n} from \"./types.js\";\n\nexport {\n  createMcpApp,\n  registerJsonRenderTool,\n  registerJsonRenderResource,\n} from \"./server.js\";\n"
  },
  {
    "path": "packages/mcp/src/server.ts",
    "content": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type {\n  CreateMcpAppOptions,\n  RegisterToolOptions,\n  RegisterResourceOptions,\n} from \"./types.js\";\n\nconst RESOURCE_MIME_TYPE = \"text/html;profile=mcp-app\";\n\n/**\n * Dynamically import the ext-apps server helpers to avoid CJS/ESM type\n * mismatches at compile time while still getting the proper runtime\n * `_meta.ui` normalization that hosts require.\n */\nasync function getExtApps() {\n  const mod = await import(\"@modelcontextprotocol/ext-apps/server\");\n  return mod;\n}\n\n/**\n * Register a json-render tool on an existing MCP server.\n *\n * Uses `registerAppTool` from `@modelcontextprotocol/ext-apps/server`\n * so that MCP Apps-capable hosts (Claude, VS Code, Cursor, ChatGPT)\n * see the `_meta.ui.resourceUri` in the tool listing and know to\n * fetch and render the `ui://` resource as an interactive iframe.\n *\n * The tool accepts a json-render spec as input and returns it as text\n * content, which the iframe receives via `ontoolresult`.\n */\nexport async function registerJsonRenderTool(\n  server: McpServer,\n  options: RegisterToolOptions,\n): Promise<void> {\n  const { catalog, name, title, description, resourceUri } = options;\n  const { registerAppTool } = await getExtApps();\n\n  const specZodSchema = catalog.zodSchema();\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  (registerAppTool as any)(\n    server,\n    name,\n    {\n      title,\n      description,\n      inputSchema: { spec: specZodSchema },\n      _meta: { ui: { resourceUri } },\n    },\n    async (args: { spec?: unknown }) => {\n      const spec = args.spec;\n      const validation = catalog.validate(spec);\n      const validSpec = validation.success ? validation.data : spec;\n\n      return {\n        content: [\n          {\n            type: \"text\" as const,\n            text: JSON.stringify(validSpec),\n          },\n        ],\n      };\n    },\n  );\n}\n\n/**\n * Register a json-render UI resource on an existing MCP server.\n *\n * The resource serves the self-contained HTML page that renders\n * json-render specs received from tool results.\n */\nexport async function registerJsonRenderResource(\n  server: McpServer,\n  options: RegisterResourceOptions,\n): Promise<void> {\n  const { resourceUri, html } = options;\n  const { registerAppResource, RESOURCE_MIME_TYPE: mimeType } =\n    await getExtApps();\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  (registerAppResource as any)(\n    server,\n    resourceUri,\n    resourceUri,\n    { mimeType },\n    async () => ({\n      contents: [\n        {\n          uri: resourceUri,\n          mimeType,\n          text: html,\n          _meta: {\n            ui: {\n              csp: {\n                resourceDomains: [\"https:\"],\n                connectDomains: [\"https:\"],\n              },\n            },\n          },\n        },\n      ],\n    }),\n  );\n}\n\n/**\n * Create a fully-configured MCP server that serves a json-render catalog\n * as an MCP App.\n *\n * This is the main entry point for most users. It creates an `McpServer`,\n * registers the render tool and UI resource, and returns the server\n * ready for transport connection.\n *\n * @example\n * ```ts\n * import { createMcpApp } from \"@json-render/mcp\";\n *\n * const server = createMcpApp({\n *   name: \"My Dashboard\",\n *   version: \"1.0.0\",\n *   catalog: myCatalog,\n *   html: myBundledHtml,\n * });\n *\n * // Connect via stdio\n * import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n * await server.connect(new StdioServerTransport());\n * ```\n */\nexport async function createMcpApp(\n  options: CreateMcpAppOptions,\n): Promise<McpServer> {\n  const { name, version, catalog, html, tool } = options;\n\n  const toolName = tool?.name ?? \"render-ui\";\n  const toolTitle = tool?.title ?? \"Render UI\";\n  const resourceUri = `ui://${toolName}/view.html`;\n\n  const catalogPrompt = catalog.prompt();\n  const toolDescription =\n    tool?.description ??\n    `Render an interactive UI. The spec argument must be a json-render spec conforming to the catalog.\\n\\n${catalogPrompt}`;\n\n  const server = new McpServer({ name, version });\n\n  await registerJsonRenderTool(server, {\n    catalog,\n    name: toolName,\n    title: toolTitle,\n    description: toolDescription,\n    resourceUri,\n  });\n\n  await registerJsonRenderResource(server, { resourceUri, html });\n\n  return server;\n}\n"
  },
  {
    "path": "packages/mcp/src/types.ts",
    "content": "import type { Catalog } from \"@json-render/core\";\n\n/**\n * Options for creating an MCP App server backed by a json-render catalog.\n */\nexport interface CreateMcpAppOptions {\n  /** Display name for the MCP server shown in client UIs. */\n  name: string;\n  /** Semantic version of the MCP server. */\n  version: string;\n  /** The json-render catalog defining available components and actions. */\n  catalog: Catalog;\n  /**\n   * Pre-built HTML string for the UI resource.\n   * Generate this with `buildAppHtml` from `@json-render/mcp/app` or\n   * provide your own self-contained HTML page.\n   */\n  html: string;\n  /**\n   * Optional tool configuration overrides.\n   */\n  tool?: McpToolOptions;\n}\n\n/**\n * Options for configuring the MCP tool that renders json-render specs.\n */\nexport interface McpToolOptions {\n  /** Tool name exposed to the LLM. Defaults to `\"render-ui\"`. */\n  name?: string;\n  /** Human-readable title. Defaults to `\"Render UI\"`. */\n  title?: string;\n  /** Tool description shown to the LLM. When omitted, a description is\n   *  auto-generated from the catalog prompt. */\n  description?: string;\n}\n\n/**\n * Options for registering the MCP App tool.\n */\nexport interface RegisterToolOptions {\n  /** The json-render catalog. */\n  catalog: Catalog;\n  /** Tool name. */\n  name: string;\n  /** Tool title. */\n  title: string;\n  /** Tool description. */\n  description: string;\n  /** The `ui://` resource URI this tool's view is served from. */\n  resourceUri: string;\n}\n\n/**\n * Options for registering the MCP App UI resource.\n */\nexport interface RegisterResourceOptions {\n  /** The `ui://` resource URI. */\n  resourceUri: string;\n  /** Self-contained HTML string for the view. */\n  html: string;\n}\n"
  },
  {
    "path": "packages/mcp/src/use-json-render-app.ts",
    "content": "import { useState, useEffect, useCallback, useRef } from \"react\";\nimport type { Spec } from \"@json-render/core\";\nimport { App } from \"@modelcontextprotocol/ext-apps\";\n\n/**\n * Options for the `useJsonRenderApp` hook.\n */\nexport interface UseJsonRenderAppOptions {\n  /** App name shown during initialization. Defaults to `\"json-render\"`. */\n  name?: string;\n  /** App version. Defaults to `\"1.0.0\"`. */\n  version?: string;\n}\n\n/**\n * Return value of `useJsonRenderApp`.\n */\nexport interface UseJsonRenderAppReturn {\n  /** The current json-render spec (null until the first tool result). */\n  spec: Spec | null;\n  /** Whether the app is still connecting to the host. */\n  connecting: boolean;\n  /** Whether the app is connected to the host. */\n  connected: boolean;\n  /** Connection error, if any. */\n  error: Error | null;\n  /** Whether the spec is still being received / parsed. */\n  loading: boolean;\n  /** The underlying MCP App instance. */\n  app: App | null;\n  /**\n   * Call a tool on the MCP server and update the spec from the result.\n   * Useful for refresh / drill-down interactions.\n   */\n  callServerTool: (\n    name: string,\n    args?: Record<string, unknown>,\n  ) => Promise<void>;\n}\n\ninterface ToolResultContent {\n  type: string;\n  text?: string;\n}\n\nfunction parseSpecFromToolResult(result: {\n  content?: ToolResultContent[];\n}): Spec | null {\n  const textContent = result.content?.find(\n    (c: ToolResultContent) => c.type === \"text\",\n  );\n  if (!textContent?.text) return null;\n  try {\n    const parsed = JSON.parse(textContent.text);\n    if (parsed && typeof parsed === \"object\" && \"spec\" in parsed) {\n      return parsed.spec as Spec;\n    }\n    return parsed as Spec;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * React hook that connects to the MCP host, listens for tool results,\n * and maintains the current json-render spec.\n *\n * Follows the official MCP Apps pattern: create an `App` instance,\n * register the `ontoolresult` handler, then call `app.connect()`\n * which internally creates a PostMessageTransport to the host.\n */\nexport function useJsonRenderApp(\n  options: UseJsonRenderAppOptions = {},\n): UseJsonRenderAppReturn {\n  const { name = \"json-render\", version = \"1.0.0\" } = options;\n\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [connected, setConnected] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const appRef = useRef<App | null>(null);\n\n  useEffect(() => {\n    const app = new App({ name, version });\n    appRef.current = app;\n\n    app.ontoolresult = (result: { content?: ToolResultContent[] }) => {\n      const parsed = parseSpecFromToolResult(result);\n      if (parsed) {\n        setSpec(parsed);\n        setLoading(false);\n      }\n    };\n\n    // Let the App class handle transport creation internally,\n    // matching the official MCP Apps quickstart pattern.\n    app\n      .connect()\n      .then(() => {\n        setConnected(true);\n      })\n      .catch((err: unknown) => {\n        setError(err instanceof Error ? err : new Error(String(err)));\n      });\n\n    return () => {\n      app.close().catch(() => {});\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const callServerTool = useCallback(\n    async (toolName: string, args: Record<string, unknown> = {}) => {\n      if (!appRef.current) return;\n      setLoading(true);\n      try {\n        const result = await appRef.current.callServerTool({\n          name: toolName,\n          arguments: args,\n        });\n        const parsed = parseSpecFromToolResult(result);\n        if (parsed) setSpec(parsed);\n      } finally {\n        setLoading(false);\n      }\n    },\n    [],\n  );\n\n  return {\n    spec,\n    connecting: !connected && !error,\n    connected,\n    error,\n    loading,\n    app: appRef.current,\n    callServerTool,\n  };\n}\n"
  },
  {
    "path": "packages/mcp/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/mcp/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/app.ts\", \"src/build-app-html-entry.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"react\",\n    \"react-dom\",\n    \"@json-render/core\",\n    \"@json-render/react\",\n    \"@modelcontextprotocol/sdk\",\n    \"@modelcontextprotocol/ext-apps\",\n  ],\n});\n"
  },
  {
    "path": "packages/react/CHANGELOG.md",
    "content": "# @json-render/react\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Minor Changes\n\n- 9cef4e9: Dynamic forms, Vue renderer, XState Store adapter, and computed values.\n\n  ### New: `@json-render/vue` Package\n\n  Vue 3 renderer for json-render. Full feature parity with `@json-render/react` including data binding, visibility conditions, actions, validation, repeat scopes, and streaming.\n  - `defineRegistry` — create type-safe component registries from catalogs\n  - `Renderer` — render specs as Vue component trees\n  - Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`\n  - Composables: `useStateStore`, `useStateValue`, `useStateBinding`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`\n  - Streaming: `useUIStream`, `useChatUI`\n  - External store support via `StateStore` interface\n\n  ### New: `@json-render/xstate` Package\n\n  XState Store (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend.\n  - `xstateStoreStateStore({ atom })` — creates a `StateStore` from an `@xstate/store` atom\n  - Requires `@xstate/store` v3+\n\n  ### New: `$computed` Expressions\n\n  Call registered functions from prop expressions:\n  - `{ \"$computed\": \"functionName\", \"args\": { \"key\": <expression> } }` — calls a named function with resolved args\n  - Functions registered via catalog and provided at runtime through `functions` prop on `JSONUIProvider` / `createRenderer`\n  - `ComputedFunction` type exported from `@json-render/core`\n\n  ### New: `$template` Expressions\n\n  Interpolate state values into strings:\n  - `{ \"$template\": \"Hello, ${/user/name}!\" }` — replaces `${/path}` references with state values\n  - Missing paths resolve to empty string\n\n  ### New: State Watchers\n\n  React to state changes by triggering actions:\n  - `watch` field on elements maps state paths to action bindings\n  - Fires when watched values change (not on initial render)\n  - Supports cascading dependencies (e.g. country → city loading)\n  - `watch` is a top-level field on elements (sibling of type/props/children), not inside props\n  - Spec validator detects and auto-fixes `watch` placed inside props\n\n  ### New: Cross-Field Validation Functions\n\n  New built-in validation functions for cross-field comparisons:\n  - `equalTo` — alias for `matches` with clearer semantics\n  - `lessThan` — value must be less than another field (numbers, strings, coerced)\n  - `greaterThan` — value must be greater than another field\n  - `requiredIf` — required only when a condition field is truthy\n  - Validation args now resolve through `resolvePropValue` for consistent `$state` expression handling\n\n  ### New: `validateForm` Action (React)\n\n  Built-in action that validates all registered form fields at once:\n  - Runs `validateAll()` synchronously and writes `{ valid, errors }` to state\n  - Default state path: `/formValidation` (configurable via `statePath` param)\n  - Added to React schema's built-in actions list\n\n  ### Improved: shadcn/ui Validation\n\n  All form components now support validation:\n  - Checkbox, Radio, Switch — added `checks` and `validateOn` props\n  - Input, Textarea, Select — added `validateOn` prop (controls timing: change/blur/submit)\n  - Shared validation schemas reduce catalog definition duplication\n\n  ### Improved: React Provider Tree\n\n  Reordered provider nesting so `ValidationProvider` wraps `ActionProvider`, enabling `validateForm` to access validation state. Added `useOptionalValidation` hook for non-throwing access.\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- b103676: Fix install failure caused by `@internal/react-state` (a private workspace package) being listed as a published dependency. The internal package is now bundled into each renderer's output at build time, so consumers no longer need to resolve it from npm.\n  - @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @internal/react-state@0.8.1\n\n## 0.8.0\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n\n## 0.7.0\n\n### Minor Changes\n\n- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.\n\n  ### New: `@json-render/shadcn` Package\n\n  Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.\n  - `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)\n  - `shadcnComponents` — React implementations for all components\n  - Layout: Card, Stack, Grid, Separator\n  - Navigation: Tabs, Accordion, Collapsible, Pagination\n  - Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu\n  - Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table\n  - Feedback: Progress, Skeleton, Spinner\n  - Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup\n\n  ### New: Event Handles (`on()`)\n\n  Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:\n  - `emit()` — fire the event\n  - `shouldPreventDefault` — whether any action binding requested `preventDefault`\n  - `bound` — whether any handler is bound to this event\n\n  ### New: `BaseComponentProps`\n\n  Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.\n\n  ### New: Built-in Actions in Schema\n\n  Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.\n\n  ### New: `preventDefault` on `ActionBinding`\n\n  Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.\n\n  ### Improved: Stream Transform Text Block Splitting\n\n  `createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.\n\n  ### Improved: `defineRegistry` Actions Requirement\n\n  `defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.\n\n### Patch Changes\n\n- Updated dependencies [2d70fab]\n  - @json-render/core@0.7.0\n\n## 0.6.1\n\n### Patch Changes\n\n- 43ad534: Fix infinite re-render loop caused by multiple unbound form inputs (Input, Textarea, Select) all registering field validation at the same empty path with different `checks` configs, causing them to overwrite each other endlessly. Stabilize context values in ActionProvider, ValidationProvider, and useUIStream by using refs for state/callbacks, preventing unnecessary re-render cascades on every state update.\n  - @json-render/core@0.6.1\n\n## 0.6.1\n\n### Patch Changes\n\n- ea97aff: Fix infinite re-render loop caused by multiple unbound form inputs (Input, Textarea, Select) all registering field validation at the same empty path with different `checks` configs, causing them to overwrite each other endlessly. Stabilize context values in ActionProvider, ValidationProvider, and useUIStream by using refs for state/callbacks, preventing unnecessary re-render cascades on every state update.\n- Updated dependencies [ea97aff]\n  - @json-render/core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 06b8745: Chat mode (inline GenUI), AI SDK integration, two-way binding, and expression-based visibility/props.\n\n  ### New: Chat Mode (Inline GenUI)\n\n  Two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets AI respond conversationally with embedded UI specs — ideal for chatbots and copilot experiences.\n  - `catalog.prompt({ mode: \"chat\" })` generates a chat-aware system prompt\n  - `pipeJsonRender()` server-side transform separates text from JSONL patches in a mixed stream\n  - `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n\n  ### New: AI SDK Integration\n\n  First-class Vercel AI SDK support with typed data parts and stream utilities.\n  - `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n  - `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n  - `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n  ### New: React Chat Hooks\n  - `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n  - `useJsonRenderMessage()` — extract spec + text from a message's parts array\n  - `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n  - `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem` expressions\n\n  ### New: Two-Way Binding\n\n  Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state.\n\n  ### New: Expression-Based Props and Visibility\n\n  Replaced string token rewriting with structured expression objects:\n  - Props: `{ $state: \"/path\" }`, `{ $item: \"field\" }`, `{ $index: true }`\n  - Visibility: `{ $state: \"/path\", eq: \"value\" }`, `{ $item: \"active\" }`, `{ $index: true, gt: 0 }`\n  - Logic: `{ $and: [...] }`, `{ $or: [...] }`, and implicit AND via arrays\n  - Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`\n\n  ### New: Utilities\n  - `applySpecPatch()` — typed convenience wrapper for applying a single patch to a Spec\n  - `nestedToFlat()` — convert nested tree specs to flat `{ root, elements }` format\n  - `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n  ### New: Chat Example\n\n  Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming UI generation.\n\n  ### Improved: Renderer\n  - `ElementRenderer` is now `React.memo`'d for better performance in repeat lists\n  - `emit` is always defined (never `undefined`) — no more optional chaining needed\n  - Action params are resolved through `resolveActionParam` supporting `$item`, `$index`, `$state`\n  - Repeat scope now passes the actual item object instead of requiring token rewriting\n\n  ### Breaking Changes\n  - **Expressions renamed**: `{ $path }` / `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }`\n  - **Visibility conditions**: `{ path }` → `{ $state }`, `{ and/or/not }` → `{ $and/$or }` with `not` as operator flag\n  - **DynamicValue**: `{ path: string }` → `{ $state: string }`\n  - **Repeat field**: `repeat.path` → `repeat.statePath`\n  - **Action params**: `path` → `statePath` in setState action params\n  - **Provider props**: `actionHandlers` → `handlers` on `JSONUIProvider`/`ActionProvider`\n  - **Auth removed**: `AuthState` type and `{ auth }` visibility conditions removed — model auth as regular state\n  - **Legacy catalog removed**: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`, `ComponentDefinition`, `CatalogConfig`, `SystemPromptOptions` removed\n  - **React exports removed**: `createRendererFromCatalog`, `rewriteRepeatTokens`\n  - **Codegen**: `traverseTree` → `traverseSpec`, `SpecVisitor` → `TreeVisitor`\n\n### Patch Changes\n\n- Updated dependencies [06b8745]\n  - @json-render/core@0.6.0\n\n## 0.5.2\n\n### Patch Changes\n\n- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.\n- Updated dependencies [429e456]\n  - @json-render/core@0.5.2\n\n## 0.5.1\n\n### Patch Changes\n\n- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.\n  - @json-render/core@0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.\n\n### Patch Changes\n\n- Updated dependencies [3d2d1ad]\n  - @json-render/core@0.5.0\n\n## 0.4.4\n\n### Patch Changes\n\n- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream\n- Updated dependencies [dd17549]\n  - @json-render/core@0.4.4\n\n## 0.4.3\n\n### Patch Changes\n\n- 61ee8e5: include remove op in system prompt\n- Updated dependencies [61ee8e5]\n  - @json-render/core@0.4.3\n\n## 0.4.2\n\n### Patch Changes\n\n- 54bce09: add defineRegistry function\n- Updated dependencies [54bce09]\n  - @json-render/core@0.4.2\n"
  },
  {
    "path": "packages/react/README.md",
    "content": "# @json-render/react\n\nReact renderer for json-render. Turn JSON specs into React components with data binding, visibility, and actions.\n\n## Installation\n\n```bash\nnpm install @json-render/react @json-render/core zod\n```\n\n## Quick Start\n\n### 1. Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"A card container\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n      }),\n      description: \"A clickable button\",\n    },\n    Input: {\n      props: z.object({\n        value: z.union([z.string(), z.record(z.unknown())]).nullable(),\n        label: z.string(),\n        placeholder: z.string().nullable(),\n      }),\n      description: \"Text input field with optional value binding\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit the form\" },\n    cancel: { description: \"Cancel and close\" },\n  },\n});\n```\n\n### 2. Define Component Implementations\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it entirely.\n\n```tsx\nimport { defineRegistry, useBoundProp } from \"@json-render/react\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => (\n      <div className=\"card\">\n        <h3>{props.title}</h3>\n        {props.description && <p>{props.description}</p>}\n        {children}\n      </div>\n    ),\n    Button: ({ props, emit }) => (\n      <button onClick={() => emit(\"press\")}>\n        {props.label}\n      </button>\n    ),\n    Input: ({ props, bindings }) => {\n      const [value, setValue] = useBoundProp(props.value, bindings?.value);\n      return (\n        <label>\n          {props.label}\n          <input\n            placeholder={props.placeholder ?? \"\"}\n            value={value ?? \"\"}\n            onChange={(e) => setValue(e.target.value)}\n          />\n        </label>\n      );\n    },\n  },\n});\n```\n\n### 3. Render Specs\n\n```tsx\nimport { Renderer, StateProvider, ActionProvider } from \"@json-render/react\";\nimport { registry } from \"./registry\";\n\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{ form: { name: \"\" } }}>\n      <ActionProvider handlers={{\n        submit: () => console.log(\"Submit\"),\n      }}>\n        <Renderer spec={spec} registry={registry} />\n      </ActionProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Spec Format\n\nThe React renderer uses a flat element map format:\n\n```typescript\ninterface Spec {\n  root: string;                          // Key of the root element\n  elements: Record<string, UIElement>;   // Flat map of elements by key\n  state?: Record<string, unknown>;       // Optional initial state\n}\n\ninterface UIElement {\n  type: string;                          // Component name from catalog\n  props: Record<string, unknown>;        // Component props\n  children?: string[];                   // Keys of child elements\n  visible?: VisibilityCondition;         // Visibility condition\n}\n```\n\nExample spec:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Welcome\" },\n      \"children\": [\"input-1\", \"btn-1\"]\n    },\n    \"input-1\": {\n      \"type\": \"Input\",\n      \"props\": {\n        \"value\": { \"$bindState\": \"/form/name\" },\n        \"label\": \"Name\",\n        \"placeholder\": \"Enter name\"\n      }\n    },\n    \"btn-1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Submit\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Contexts\n\n### StateProvider\n\nShare data across components with JSON Pointer paths:\n\n```tsx\n<StateProvider initialState={{ user: { name: \"John\" } }}>\n  {children}\n</StateProvider>\n\n// In components:\nconst { state, get, set } = useStateStore();\nconst name = get(\"/user/name\");  // \"John\"\nset(\"/user/age\", 25);\n```\n\n#### External Store (Controlled Mode)\n\nFor full control over state, pass a `StateStore` to bypass the internal state and wire json-render to any state management library (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react\";\n\n// Option 1: Use the built-in store outside of React\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>\n  {children}\n</StateProvider>\n\n// Mutate from anywhere — React will re-render automatically:\nstore.set(\"/count\", 1);\n\n// Option 2: Implement the StateStore interface with your own backend\nconst zustandStore: StateStore = {\n  get: (path) => getByPath(useStore.getState(), path),\n  set: (path, value) => useStore.setState(prev => { /* ... */ }),\n  update: (updates) => useStore.setState(prev => { /* ... */ }),\n  getSnapshot: () => useStore.getState(),\n  subscribe: (listener) => useStore.subscribe(listener),\n};\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored. The store is the single source of truth. The same `store` prop is available on `createRenderer`, `JSONUIProvider`, and `StateProvider`.\n\n### ActionProvider\n\nHandle actions from components:\n\n```tsx\n<ActionProvider\n  handlers={{\n    submit: (params) => handleSubmit(params),\n    cancel: () => handleCancel(),\n  }}\n>\n  {children}\n</ActionProvider>\n```\n\n### VisibilityProvider\n\nControl element visibility based on data:\n\n```tsx\n<VisibilityProvider>\n  {children}\n</VisibilityProvider>\n\n// Elements can use visibility conditions:\n{\n  \"type\": \"Alert\",\n  \"props\": { \"message\": \"Error!\" },\n  \"visible\": { \"$state\": \"/form/hasError\" }\n}\n```\n\n### ValidationProvider\n\nAdd field validation:\n\n```tsx\n<ValidationProvider>\n  {children}\n</ValidationProvider>\n\n// Use validation hooks:\nconst { errors, validate } = useFieldValidation(\"/form/email\", {\n  checks: [\n    { type: \"required\", message: \"Email required\" },\n    { type: \"email\", message: \"Invalid email\" },\n  ],\n});\n```\n\n## Hooks\n\n| Hook | Purpose |\n|------|---------|\n| `useStateStore()` | Access state context (`state`, `get`, `set`, `update`) |\n| `useStateValue(path)` | Get single value from state |\n| `useStateBinding(path)` | Two-way data binding (returns `[value, setValue]`) |\n| `useIsVisible(condition)` | Check if a visibility condition is met |\n| `useActions()` | Access action context |\n| `useAction(name)` | Get a single action dispatch function |\n| `useFieldValidation(path, config)` | Field validation state |\n| `useOptionalValidation()` | Non-throwing validation context (returns `null` if no provider) |\n| `useUIStream(options)` | Stream specs from an API endpoint |\n\n## Visibility Conditions\n\n```typescript\n// Truthiness check\n{ \"$state\": \"/user/isAdmin\" }\n\n// Auth state (use state path)\n{ \"$state\": \"/auth/isSignedIn\" }\n\n// Comparisons (flat style)\n{ \"$state\": \"/status\", \"eq\": \"active\" }\n{ \"$state\": \"/count\", \"gt\": 10 }\n\n// Negation\n{ \"$state\": \"/maintenance\", \"not\": true }\n\n// Multiple conditions (implicit AND)\n[\n  { \"$state\": \"/feature/enabled\" },\n  { \"$state\": \"/maintenance\", \"not\": true }\n]\n\n// Always / never\ntrue   // always visible\nfalse  // never visible\n```\n\nTypeScript helpers from `@json-render/core`:\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\nvisibility.when(\"/path\")       // { $state: \"/path\" }\nvisibility.unless(\"/path\")     // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)    // { $state: \"/path\", eq: val }\nvisibility.neq(\"/path\", val)   // { $state: \"/path\", neq: val }\nvisibility.and(cond1, cond2)  // { $and: [cond1, cond2] }\nvisibility.always             // true\nvisibility.never              // false\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can use data-driven expressions that resolve at render time. The renderer resolves these transparently before passing props to components.\n\n```json\n{\n  \"type\": \"Badge\",\n  \"props\": {\n    \"label\": { \"$state\": \"/user/role\" },\n    \"color\": {\n      \"$cond\": { \"$state\": \"/user/role\", \"eq\": \"admin\" },\n      \"$then\": \"red\",\n      \"$else\": \"gray\"\n    }\n  }\n}\n```\n\nFor two-way binding, use `{ \"$bindState\": \"/path\" }` on the natural value prop (e.g. `value`, `checked`, `pressed`). Inside repeat scopes, use `{ \"$bindItem\": \"field\" }` instead. Components receive resolved `bindings` with the state path for each bound prop; use `useBoundProp(props.value, bindings?.value)` to get `[value, setValue]`.\n\n### `$template` and `$computed`\n\n```json\n{\n  \"label\": { \"$template\": \"Hello, ${/user/name}!\" },\n  \"fullName\": {\n    \"$computed\": \"fullName\",\n    \"args\": {\n      \"first\": { \"$state\": \"/form/firstName\" },\n      \"last\": { \"$state\": \"/form/lastName\" }\n    }\n  }\n}\n```\n\nRegister functions via the `functions` prop on `JSONUIProvider` or `createRenderer`:\n\n```tsx\n<JSONUIProvider\n  spec={spec}\n  catalog={catalog}\n  functions={{ fullName: (args) => `${args.first} ${args.last}` }}\n>\n```\n\nSee [@json-render/core](../core/README.md) for full expression syntax.\n\n## State Watchers\n\nElements can declare a `watch` field to trigger actions when state values change:\n\n```json\n{\n  \"type\": \"Select\",\n  \"props\": {\n    \"label\": \"Country\",\n    \"value\": { \"$bindState\": \"/form/country\" },\n    \"options\": [\"US\", \"Canada\", \"UK\"]\n  },\n  \"watch\": {\n    \"/form/country\": {\n      \"action\": \"loadCities\",\n      \"params\": { \"country\": { \"$state\": \"/form/country\" } }\n    }\n  },\n  \"children\": []\n}\n```\n\n`watch` is a top-level field on elements (sibling of `type`/`props`/`children`), not inside `props`. Watchers only fire on value changes, not on initial render.\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 your catalog's `actions`:\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Switch Tab\" },\n  \"on\": {\n    \"press\": {\n      \"action\": \"setState\",\n      \"params\": { \"statePath\": \"/activeTab\", \"value\": \"settings\" }\n    }\n  },\n  \"children\": []\n}\n```\n\n### `validateForm`\n\nValidate all registered form fields at once and write the result to state:\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": {\n    \"press\": [\n      { \"action\": \"validateForm\", \"params\": { \"statePath\": \"/formResult\" } },\n      { \"action\": \"submitForm\" }\n    ]\n  },\n  \"children\": []\n}\n```\n\nWrites `{ valid: boolean, errors: Record<string, string[]> }` to the specified state path (defaults to `/formValidation`).\n\n## Component Props\n\nWhen using `defineRegistry`, components receive these props:\n\n```typescript\ninterface ComponentContext<P> {\n  props: P;                    // Typed props from the catalog (expressions resolved)\n  children?: React.ReactNode;  // Rendered children\n  emit: (event: string) => void;   // Emit a named event (always defined)\n  on: (event: string) => EventHandle; // Get event handle with metadata\n  loading?: boolean;           // Whether the parent is loading\n  bindings?: Record<string, string>;  // State paths for $bindState/$bindItem expressions (e.g. bindings.value)\n}\n\ninterface EventHandle {\n  emit: () => void;            // Fire the event\n  shouldPreventDefault: boolean; // Whether any binding requested preventDefault\n  bound: boolean;              // Whether any handler is bound\n}\n```\n\nUse `emit(\"press\")` for simple event firing. Use `on(\"click\")` when you need to check metadata like `shouldPreventDefault` or `bound`:\n\n```tsx\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return (\n    <a\n      href={props.href}\n      onClick={(e) => {\n        if (click.shouldPreventDefault) e.preventDefault();\n        click.emit();\n      }}\n    >\n      {props.label}\n    </a>\n  );\n},\n```\n\nUse `bindings?.value`, `bindings?.checked`, etc. with `useBoundProp()` for two-way bound form components.\n\n### `BaseComponentProps`\n\nFor building reusable component libraries that are not tied to a specific catalog (e.g. `@json-render/shadcn`), use the catalog-agnostic `BaseComponentProps` type:\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## Generate AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt();\n// Returns detailed prompt with component/action descriptions\n```\n\n## Full Example\n\n```tsx\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { defineRegistry, Renderer } from \"@json-render/react\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Greeting: {\n      props: z.object({ name: z.string() }),\n      description: \"Displays a greeting\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Greeting: ({ props }) => <h1>Hello, {props.name}!</h1>,\n  },\n});\n\nconst spec = {\n  root: \"greeting-1\",\n  elements: {\n    \"greeting-1\": {\n      type: \"Greeting\",\n      props: { name: \"World\" },\n      children: [],\n    },\n  },\n};\n\nfunction App() {\n  return <Renderer spec={spec} registry={registry} />;\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| `schema` | Element tree schema (includes built-in 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| `useUIStream` | Stream specs from an API endpoint |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n\n### Types\n\n| Export | Purpose |\n|--------|---------|\n| `ComponentContext` | Typed component render function context (catalog-aware) |\n| `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries |\n| `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` |\n| `ComponentFn` | Component render function type |\n| `SetState` | State setter type |\n| `StateModel` | State model type |\n| `StateStore` | Interface for plugging in external state management |\n"
  },
  {
    "path": "packages/react/package.json",
    "content": "{\n  \"name\": \"@json-render/react\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"React renderer for @json-render/core. JSON becomes React components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"react\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/react\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./schema\": {\n      \"types\": \"./dist/schema.d.ts\",\n      \"import\": \"./dist/schema.mjs\",\n      \"require\": \"./dist/schema.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/react-state\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.2.3\"\n  }\n}\n"
  },
  {
    "path": "packages/react/src/catalog-types.ts",
    "content": "import type { ReactNode } from \"react\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\n/**\n * State setter function for updating application state\n */\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\n/**\n * Handle returned by the `on()` function for a specific event.\n * Provides metadata about the event binding and a method to fire it.\n *\n * @example\n * ```ts\n * const press = on(\"press\");\n * if (press.shouldPreventDefault) e.preventDefault();\n * press.emit();\n * ```\n */\nexport interface EventHandle {\n  /** Fire the event (resolve action bindings) */\n  emit: () => void;\n  /** Whether any binding requested preventDefault */\n  shouldPreventDefault: boolean;\n  /** Whether any handler is bound to this event */\n  bound: boolean;\n}\n\n/**\n * Catalog-agnostic base type for component render function arguments.\n * Use this when building reusable component libraries (e.g. `@json-render/shadcn`)\n * that are not tied to a specific catalog.\n *\n * @example\n * ```ts\n * const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) => (\n *   <div>{props.title}{children}</div>\n * );\n * ```\n */\nexport interface BaseComponentProps<P = Record<string, unknown>> {\n  props: P;\n  children?: ReactNode;\n  /** Simple event emitter (shorthand). Fires the event and returns void. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\n/**\n * Context passed to component render functions\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => {\n *   return <button onClick={() => ctx.emit(\"press\")}>{ctx.props.label}</button>\n * }\n */\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> extends BaseComponentProps<InferComponentProps<C, K>> {}\n\n/**\n * Component render function type for React\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = ({ props, emit }) => (\n *   <button onClick={() => emit(\"press\")}>{props.label}</button>\n * );\n */\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => ReactNode;\n\n/**\n * Registry of all component render functions for a catalog\n * @example\n * const components: Components<typeof myCatalog> = {\n *   Button: ({ props }) => <button>{props.label}</button>,\n *   Input: ({ props }) => <input placeholder={props.placeholder} />,\n * };\n */\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n\n// =============================================================================\n// Action Types\n// =============================================================================\n\n/**\n * Action handler function type\n * @example\n * const viewCustomers: ActionFn<typeof catalog, 'viewCustomers'> = async (params, setState) => {\n *   const data = await fetch('/api/customers');\n *   setState(prev => ({ ...prev, customers: data }));\n * };\n */\nexport type ActionFn<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = (\n  params: InferActionParams<C, K> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Registry of all action handlers for a catalog\n * @example\n * const actions: Actions<typeof myCatalog> = {\n *   viewCustomers: async (params, setState) => { ... },\n *   createCustomer: async (params, setState) => { ... },\n * };\n */\nexport type Actions<C extends Catalog> = {\n  [K in keyof InferCatalogActions<C>]: ActionFn<C, K>;\n};\n\n/**\n * True when the catalog declares at least one action, false otherwise.\n * Used by defineRegistry to conditionally require the `actions` field.\n */\nexport type CatalogHasActions<C extends Catalog> = [\n  InferCatalogActions<C>,\n] extends [never]\n  ? false\n  : [keyof InferCatalogActions<C>] extends [never]\n    ? false\n    : true;\n"
  },
  {
    "path": "packages/react/src/chained-actions.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport React from \"react\";\nimport { render, act, fireEvent, screen } from \"@testing-library/react\";\nimport type { Spec } from \"@json-render/core\";\nimport {\n  JSONUIProvider,\n  Renderer,\n  type ComponentRenderProps,\n} from \"./renderer\";\nimport { useStateStore } from \"./contexts/state\";\n\n/**\n * Minimal Button component that calls emit(\"press\") on click.\n */\nfunction Button({ element, emit }: ComponentRenderProps<{ label: string }>) {\n  return (\n    <button data-testid=\"btn\" onClick={() => emit(\"press\")}>\n      {element.props.label}\n    </button>\n  );\n}\n\n/**\n * Text component that renders its `text` prop.\n */\nfunction Text({ element }: ComponentRenderProps<{ text: unknown }>) {\n  const value = element.props.text;\n  return (\n    <span data-testid={`text-${element.type}`}>\n      {typeof value === \"string\" ? value : JSON.stringify(value)}\n    </span>\n  );\n}\n\n/**\n * Helper component that reads live state and exposes it via a data attribute\n * so we can assert against the store after actions fire.\n */\nfunction StateProbe() {\n  const { state } = useStateStore();\n  return <pre data-testid=\"state-probe\">{JSON.stringify(state)}</pre>;\n}\n\nconst registry = {\n  Button,\n  Text,\n};\n\ndescribe(\"chained actions: live $state resolution (#141)\", () => {\n  it(\"setState after pushState sees the post-push value via $state\", async () => {\n    const spec: Spec = {\n      state: { items: [\"initial\"], observed: \"not yet set\" },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Add Item\" },\n          on: {\n            press: [\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"new-item\" },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/observed\",\n                  value: { $state: \"/items\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(<App />);\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const probe = screen.getByTestId(\"state-probe\");\n    const state = JSON.parse(probe.textContent!);\n\n    expect(state.items).toEqual([\"initial\", \"new-item\"]);\n    expect(state.observed).toEqual([\"initial\", \"new-item\"]);\n  });\n\n  it(\"multiple pushState + setState chain resolves correctly\", async () => {\n    const spec: Spec = {\n      state: { items: [], snapshot: null },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Go\" },\n          on: {\n            press: [\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"a\" },\n              },\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"b\" },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/snapshot\",\n                  value: { $state: \"/items\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(<App />);\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const state = JSON.parse(screen.getByTestId(\"state-probe\").textContent!);\n\n    expect(state.items).toEqual([\"a\", \"b\"]);\n    expect(state.snapshot).toEqual([\"a\", \"b\"]);\n  });\n\n  it(\"setState reading a path mutated by an earlier setState sees fresh value\", async () => {\n    const spec: Spec = {\n      state: { counter: 0, counterCopy: -1 },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Go\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/counter\", value: 42 },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/counterCopy\",\n                  value: { $state: \"/counter\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(<App />);\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const state = JSON.parse(screen.getByTestId(\"state-probe\").textContent!);\n\n    expect(state.counter).toBe(42);\n    expect(state.counterCopy).toBe(42);\n  });\n});\n"
  },
  {
    "path": "packages/react/src/contexts/actions.tsx",
    "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\nimport { useOptionalValidation } from \"./validation\";\n\n/**\n * Generate a unique ID for use with the \"$id\" token.\n * Combines a timestamp with a random suffix for uniqueness.\n */\nlet idCounter = 0;\nfunction generateUniqueId(): string {\n  idCounter += 1;\n  return `${Date.now()}-${idCounter}`;\n}\n\n/**\n * Deep-resolve dynamic value references within an object.\n *\n * Supported tokens:\n * - `{ $state: \"/statePath\" }` - read a value from state\n * - `\"$id\"` (string) or `{ \"$id\": true }` - generate a unique ID\n *\n * This allows pushState values to contain references to current state\n * and auto-generated IDs.\n */\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  // \"$id\" string token -> generate unique ID\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    // { $state: \"/foo\" } -> read from state\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    // { \"$id\": true } -> generate unique ID (single-key object)\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  // Recurse into arrays\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  // Recurse into plain objects\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\n/**\n * Pending confirmation state\n */\nexport interface PendingConfirmation {\n  /** The resolved action */\n  action: ResolvedAction;\n  /** The action handler */\n  handler: ActionHandler;\n  /** Resolve callback */\n  resolve: () => void;\n  /** Reject callback */\n  reject: () => void;\n}\n\n/**\n * Action context value\n */\nexport interface ActionContextValue {\n  /** Registered action handlers */\n  handlers: Record<string, ActionHandler>;\n  /** Currently loading action names */\n  loadingActions: Set<string>;\n  /** Pending confirmation dialog */\n  pendingConfirmation: PendingConfirmation | null;\n  /** Execute an action binding */\n  execute: (binding: ActionBinding) => Promise<void>;\n  /** Confirm the pending action */\n  confirm: () => void;\n  /** Cancel the pending action */\n  cancel: () => void;\n  /** Register an action handler */\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ActionContext = createContext<ActionContextValue | null>(null);\n\n/**\n * Props for ActionProvider\n */\nexport interface ActionProviderProps {\n  /** Initial action handlers */\n  handlers?: Record<string, ActionHandler>;\n  /** Navigation function */\n  navigate?: (path: string) => void;\n  children: ReactNode;\n}\n\n/**\n * Provider for action execution\n */\nexport function ActionProvider({\n  handlers: initialHandlers = {},\n  navigate,\n  children,\n}: ActionProviderProps) {\n  const { get, set, getSnapshot } = useStateStore();\n  const validation = useOptionalValidation();\n\n  const [handlers, setHandlers] =\n    useState<Record<string, ActionHandler>>(initialHandlers);\n  const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());\n  const [pendingConfirmation, setPendingConfirmation] =\n    useState<PendingConfirmation | null>(null);\n\n  const registerHandler = useCallback(\n    (name: string, handler: ActionHandler) => {\n      setHandlers((prev) => ({ ...prev, [name]: handler }));\n    },\n    [],\n  );\n\n  const execute = useCallback(\n    async (binding: ActionBinding) => {\n      const resolved = resolveAction(binding, getSnapshot());\n\n      // Built-in: setState updates the StateProvider state directly\n      if (resolved.action === \"setState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const value = resolved.params.value;\n        if (statePath) {\n          set(statePath, value);\n        }\n        return;\n      }\n\n      // Built-in: pushState appends an item to an array in state.\n      // Supports dynamic values inside the value object via { $state: \"/...\" } syntax.\n      if (resolved.action === \"pushState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const rawValue = resolved.params.value;\n        if (statePath) {\n          const resolvedValue = deepResolveValue(rawValue, get);\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(statePath, [...arr, resolvedValue]);\n          // Optionally clear a state path after pushing (e.g. clear the input)\n          const clearStatePath = resolved.params.clearStatePath as\n            | string\n            | undefined;\n          if (clearStatePath) {\n            set(clearStatePath, \"\");\n          }\n        }\n        return;\n      }\n\n      // Built-in: removeState removes an item from an array in state by index.\n      if (resolved.action === \"removeState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const index = resolved.params.index as number;\n        if (statePath !== undefined && index !== undefined) {\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(\n            statePath,\n            arr.filter((_, i) => i !== index),\n          );\n        }\n        return;\n      }\n\n      // Built-in: push navigates to a new screen by updating state.\n      // Pushes the current screen onto /navStack and sets /currentScreen.\n      if (resolved.action === \"push\" && resolved.params) {\n        const screen = resolved.params.screen as string;\n        if (screen) {\n          const currentScreen = get(\"/currentScreen\") as string | undefined;\n          const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n          if (currentScreen) {\n            set(\"/navStack\", [...navStack, currentScreen]);\n          } else {\n            // No current screen set yet -- push a sentinel so pop returns here\n            set(\"/navStack\", [...navStack, \"\"]);\n          }\n          set(\"/currentScreen\", screen);\n        }\n        return;\n      }\n\n      // Built-in: pop navigates back to the previous screen.\n      // Pops the last entry from /navStack and restores /currentScreen.\n      if (resolved.action === \"pop\") {\n        const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n        if (navStack.length > 0) {\n          const previousScreen = navStack[navStack.length - 1];\n          set(\"/navStack\", navStack.slice(0, -1));\n          if (previousScreen) {\n            set(\"/currentScreen\", previousScreen);\n          } else {\n            set(\"/currentScreen\", undefined);\n          }\n        }\n        return;\n      }\n\n      // Built-in: validateForm triggers validateAll from the ValidationProvider\n      // and writes the result to a state path (default: /formValidation).\n      // IMPORTANT: validateAll() is synchronous — it runs all registered field\n      // validations and returns immediately. This guarantees that the next action\n      // in a sequential list (e.g. [validateForm, submitForm]) can read the\n      // validation result from state without awaiting an extra tick.\n      if (resolved.action === \"validateForm\") {\n        const validateAll = validation?.validateAll;\n        if (!validateAll) {\n          console.warn(\n            \"validateForm action was dispatched but no ValidationProvider is connected. \" +\n              \"Ensure ValidationProvider is rendered inside the provider tree.\",\n          );\n          return;\n        }\n        const valid = validateAll();\n        const errors: Record<string, string[]> = {};\n        for (const [path, fs] of Object.entries(validation.fieldStates)) {\n          if (fs.result && !fs.result.valid) {\n            errors[path] = fs.result.errors;\n          }\n        }\n        const statePath =\n          (resolved.params?.statePath as string) || \"/formValidation\";\n        set(statePath, { valid, errors });\n        return;\n      }\n\n      const handler = handlers[resolved.action];\n\n      if (!handler) {\n        console.warn(`No handler registered for action: ${resolved.action}`);\n        return;\n      }\n\n      // If confirmation is required, show dialog\n      if (resolved.confirm) {\n        return new Promise<void>((resolve, reject) => {\n          setPendingConfirmation({\n            action: resolved,\n            handler,\n            resolve: () => {\n              setPendingConfirmation(null);\n              resolve();\n            },\n            reject: () => {\n              setPendingConfirmation(null);\n              reject(new Error(\"Action cancelled\"));\n            },\n          });\n        }).then(async () => {\n          setLoadingActions((prev) => new Set(prev).add(resolved.action));\n          try {\n            await executeAction({\n              action: resolved,\n              handler,\n              setState: set,\n              navigate,\n              executeAction: async (name) => {\n                const subBinding: ActionBinding = { action: name };\n                await execute(subBinding);\n              },\n            });\n          } finally {\n            setLoadingActions((prev) => {\n              const next = new Set(prev);\n              next.delete(resolved.action);\n              return next;\n            });\n          }\n        });\n      }\n\n      // Execute immediately\n      setLoadingActions((prev) => new Set(prev).add(resolved.action));\n      try {\n        await executeAction({\n          action: resolved,\n          handler,\n          setState: set,\n          navigate,\n          executeAction: async (name) => {\n            const subBinding: ActionBinding = { action: name };\n            await execute(subBinding);\n          },\n        });\n      } finally {\n        setLoadingActions((prev) => {\n          const next = new Set(prev);\n          next.delete(resolved.action);\n          return next;\n        });\n      }\n    },\n    [handlers, get, set, getSnapshot, navigate, validation],\n  );\n\n  const confirm = useCallback(() => {\n    pendingConfirmation?.resolve();\n  }, [pendingConfirmation]);\n\n  const cancel = useCallback(() => {\n    pendingConfirmation?.reject();\n  }, [pendingConfirmation]);\n\n  const value = useMemo<ActionContextValue>(\n    () => ({\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    }),\n    [\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    ],\n  );\n\n  return (\n    <ActionContext.Provider value={value}>{children}</ActionContext.Provider>\n  );\n}\n\n/**\n * Hook to access action context\n */\nexport function useActions(): ActionContextValue {\n  const ctx = useContext(ActionContext);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to execute an action binding\n */\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: boolean;\n} {\n  const { execute, loadingActions } = useActions();\n  const isLoading = loadingActions.has(binding.action);\n\n  const executeAction = useCallback(() => execute(binding), [execute, binding]);\n\n  return { execute: executeAction, isLoading };\n}\n\n/**\n * Props for ConfirmDialog component\n */\nexport interface ConfirmDialogProps {\n  /** The confirmation config */\n  confirm: ActionConfirm;\n  /** Called when confirmed */\n  onConfirm: () => void;\n  /** Called when cancelled */\n  onCancel: () => void;\n}\n\n/**\n * Default confirmation dialog component\n */\nexport function ConfirmDialog({\n  confirm,\n  onConfirm,\n  onCancel,\n}: ConfirmDialogProps) {\n  const isDanger = confirm.variant === \"danger\";\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        inset: 0,\n        backgroundColor: \"rgba(0, 0, 0, 0.5)\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        zIndex: 50,\n      }}\n      onClick={onCancel}\n    >\n      <div\n        style={{\n          backgroundColor: \"white\",\n          borderRadius: \"8px\",\n          padding: \"24px\",\n          maxWidth: \"400px\",\n          width: \"100%\",\n          boxShadow: \"0 20px 25px -5px rgba(0, 0, 0, 0.1)\",\n        }}\n        onClick={(e) => e.stopPropagation()}\n      >\n        <h3\n          style={{\n            margin: \"0 0 8px 0\",\n            fontSize: \"18px\",\n            fontWeight: 600,\n          }}\n        >\n          {confirm.title}\n        </h3>\n        <p\n          style={{\n            margin: \"0 0 24px 0\",\n            color: \"#6b7280\",\n          }}\n        >\n          {confirm.message}\n        </p>\n        <div\n          style={{\n            display: \"flex\",\n            gap: \"12px\",\n            justifyContent: \"flex-end\",\n          }}\n        >\n          <button\n            onClick={onCancel}\n            style={{\n              padding: \"8px 16px\",\n              borderRadius: \"6px\",\n              border: \"1px solid #d1d5db\",\n              backgroundColor: \"white\",\n              cursor: \"pointer\",\n            }}\n          >\n            {confirm.cancelLabel ?? \"Cancel\"}\n          </button>\n          <button\n            onClick={onConfirm}\n            style={{\n              padding: \"8px 16px\",\n              borderRadius: \"6px\",\n              border: \"none\",\n              backgroundColor: isDanger ? \"#dc2626\" : \"#3b82f6\",\n              color: \"white\",\n              cursor: \"pointer\",\n            }}\n          >\n            {confirm.confirmLabel ?? \"Confirm\"}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/react/src/contexts/repeat-scope.tsx",
    "content": "\"use client\";\n\nimport React, { createContext, useContext, type ReactNode } from \"react\";\n\n/**\n * Repeat scope value provided to child elements inside a repeated element.\n */\nexport interface RepeatScopeValue {\n  /** The current array item object */\n  item: unknown;\n  /** Index of the current item in the array */\n  index: number;\n  /** Absolute state path to the current array item (e.g. \"/todos/0\") — used for statePath two-way binding */\n  basePath: string;\n}\n\nconst RepeatScopeContext = createContext<RepeatScopeValue | null>(null);\n\n/**\n * Provides repeat scope to child elements so $item and $index expressions resolve correctly.\n */\nexport function RepeatScopeProvider({\n  item,\n  index,\n  basePath,\n  children,\n}: RepeatScopeValue & { children: ReactNode }) {\n  return (\n    <RepeatScopeContext.Provider value={{ item, index, basePath }}>\n      {children}\n    </RepeatScopeContext.Provider>\n  );\n}\n\n/**\n * Read the current repeat scope (or null if not inside a repeated element).\n */\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return useContext(RepeatScopeContext);\n}\n"
  },
  {
    "path": "packages/react/src/contexts/state.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport React from \"react\";\nimport { renderHook, act } from \"@testing-library/react\";\nimport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n} from \"./state\";\n\ndescribe(\"state re-exports (smoke test)\", () => {\n  it(\"StateProvider + useStateStore round-trip\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ count: 0 }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.get(\"/count\")).toBe(0);\n\n    act(() => {\n      result.current.set(\"/count\", 42);\n    });\n\n    expect(result.current.state.count).toBe(42);\n  });\n\n  it(\"useStateValue reads from state\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ name: \"Alice\" }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateValue(\"/name\"), { wrapper });\n\n    expect(result.current).toBe(\"Alice\");\n  });\n\n  it(\"useStateBinding returns value and setter\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ x: 1 }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateBinding(\"/x\"), { wrapper });\n\n    const [value, setValue] = result.current;\n    expect(value).toBe(1);\n    expect(typeof setValue).toBe(\"function\");\n  });\n});\n"
  },
  {
    "path": "packages/react/src/contexts/state.tsx",
    "content": "\"use client\";\n\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"@internal/react-state\";\n"
  },
  {
    "path": "packages/react/src/contexts/validation.tsx",
    "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useContext,\n  useRef,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Field validation state\n */\nexport interface FieldValidationState {\n  /** Whether the field has been touched */\n  touched: boolean;\n  /** Whether the field has been validated */\n  validated: boolean;\n  /** Validation result */\n  result: ValidationResult | null;\n}\n\n/**\n * Validation context value\n */\nexport interface ValidationContextValue {\n  /** Custom validation functions from catalog */\n  customFunctions: Record<string, ValidationFunction>;\n  /** Validation state by field path */\n  fieldStates: Record<string, FieldValidationState>;\n  /** Validate a field */\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  /** Mark field as touched */\n  touch: (path: string) => void;\n  /** Clear validation for a field */\n  clear: (path: string) => void;\n  /** Validate all fields */\n  validateAll: () => boolean;\n  /** Register field config */\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst ValidationContext = createContext<ValidationContextValue | null>(null);\n\n/**\n * Props for ValidationProvider\n */\nexport interface ValidationProviderProps {\n  /** Custom validation functions from catalog */\n  customFunctions?: Record<string, ValidationFunction>;\n  children: ReactNode;\n}\n\n/**\n * Compare two DynamicValue args records shallowly.\n * Values are primitives or { $state: string }, so shallow comparison suffices.\n */\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    // Handle { $state: string } objects\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\n/**\n * Structural equality check for ValidationConfig.\n */\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n\n  // Compare validateOn\n  if (a.validateOn !== b.validateOn) return false;\n\n  // Compare checks arrays\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n\n  return true;\n}\n\n/**\n * Provider for validation\n */\nexport function ValidationProvider({\n  customFunctions = {},\n  children,\n}: ValidationProviderProps) {\n  const { state, getSnapshot } = useStateStore();\n\n  const [fieldStates, setFieldStates] = useState<\n    Record<string, FieldValidationState>\n  >({});\n  // Mutable mirror of fieldStates for synchronous reads (e.g. reading errors\n  // immediately after validateAll() before React flushes the batched setState).\n  const fieldStatesRef = useRef<Record<string, FieldValidationState>>({});\n  const [fieldConfigs, setFieldConfigs] = useState<\n    Record<string, ValidationConfig>\n  >({});\n\n  const registerField = useCallback(\n    (path: string, config: ValidationConfig) => {\n      setFieldConfigs((prev) => {\n        const existing = prev[path];\n        // Bail out (return same reference) if config is unchanged to avoid\n        // infinite re-render loops when callers pass a fresh object each render.\n        if (existing && validationConfigEqual(existing, config)) {\n          return prev;\n        }\n        return { ...prev, [path]: config };\n      });\n    },\n    [],\n  );\n\n  const validate = useCallback(\n    (path: string, config: ValidationConfig): ValidationResult => {\n      // Read from the store directly so validation sees values written in the\n      // same synchronous handler (e.g. setValue then validate in onChange).\n      // Using React state would return the stale pre-render snapshot.\n      const currentState = getSnapshot();\n      const segments = path.split(\"/\").filter(Boolean);\n      let value: unknown = currentState;\n      for (const seg of segments) {\n        if (value != null && typeof value === \"object\") {\n          value = (value as Record<string, unknown>)[seg];\n        } else {\n          value = undefined;\n          break;\n        }\n      }\n      const result = runValidation(config, {\n        value,\n        stateModel: currentState,\n        customFunctions,\n      });\n\n      const newFieldState: FieldValidationState = {\n        touched: fieldStatesRef.current[path]?.touched ?? true,\n        validated: true,\n        result,\n      };\n      fieldStatesRef.current = {\n        ...fieldStatesRef.current,\n        [path]: newFieldState,\n      };\n      setFieldStates(fieldStatesRef.current);\n\n      return result;\n    },\n    [customFunctions, getSnapshot],\n  );\n\n  const touch = useCallback((path: string) => {\n    fieldStatesRef.current = {\n      ...fieldStatesRef.current,\n      [path]: {\n        ...fieldStatesRef.current[path],\n        touched: true,\n        validated: fieldStatesRef.current[path]?.validated ?? false,\n        result: fieldStatesRef.current[path]?.result ?? null,\n      },\n    };\n    setFieldStates(fieldStatesRef.current);\n  }, []);\n\n  const clear = useCallback((path: string) => {\n    const { [path]: _, ...rest } = fieldStatesRef.current;\n    fieldStatesRef.current = rest;\n    setFieldStates(rest);\n  }, []);\n\n  const validateAll = useCallback(() => {\n    let allValid = true;\n\n    for (const [path, config] of Object.entries(fieldConfigs)) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n\n    return allValid;\n  }, [fieldConfigs, validate]);\n\n  const value = useMemo<ValidationContextValue>(\n    () => ({\n      customFunctions,\n      // Getter returns the mutable ref so callers that read fieldStates\n      // synchronously after validateAll() see the latest values.\n      get fieldStates() {\n        return fieldStatesRef.current;\n      },\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    }),\n    [\n      customFunctions,\n      // fieldStates (React state) stays in deps so the context value object\n      // is recreated on re-render, triggering downstream consumers.\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    ],\n  );\n\n  return (\n    <ValidationContext.Provider value={value}>\n      {children}\n    </ValidationContext.Provider>\n  );\n}\n\n/**\n * Hook to access validation context\n */\nexport function useValidation(): ValidationContextValue {\n  const ctx = useContext(ValidationContext);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Non-throwing variant of useValidation.\n * Returns null when no ValidationProvider is present.\n */\nexport function useOptionalValidation(): ValidationContextValue | null {\n  return useContext(ValidationContext);\n}\n\n/**\n * Hook to get validation state for a field\n */\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: FieldValidationState;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: string[];\n  isValid: boolean;\n} {\n  const {\n    fieldStates,\n    validate: validateField,\n    touch: touchField,\n    clear: clearField,\n    registerField,\n  } = useValidation();\n\n  // Register field on mount\n  React.useEffect(() => {\n    if (path && config) {\n      registerField(path, config);\n    }\n  }, [path, config, registerField]);\n\n  const state = fieldStates[path] ?? {\n    touched: false,\n    validated: false,\n    result: null,\n  };\n\n  const validate = useCallback(\n    () => validateField(path, config ?? { checks: [] }),\n    [path, config, validateField],\n  );\n\n  const touch = useCallback(() => touchField(path), [path, touchField]);\n  const clear = useCallback(() => clearField(path), [path, clearField]);\n\n  return {\n    state,\n    validate,\n    touch,\n    clear,\n    errors: state.result?.errors ?? [],\n    isValid: state.result?.valid ?? true,\n  };\n}\n"
  },
  {
    "path": "packages/react/src/contexts/visibility.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport React from \"react\";\nimport { renderHook } from \"@testing-library/react\";\nimport { VisibilityProvider, useVisibility, useIsVisible } from \"./visibility\";\nimport { StateProvider } from \"./state\";\n\nconst createWrapper =\n  (data: Record<string, unknown> = {}) =>\n  ({ children }: { children: React.ReactNode }) => (\n    <StateProvider initialState={data}>\n      <VisibilityProvider>{children}</VisibilityProvider>\n    </StateProvider>\n  );\n\ndescribe(\"useVisibility\", () => {\n  it(\"provides isVisible function\", () => {\n    const { result } = renderHook(() => useVisibility(), {\n      wrapper: createWrapper(),\n    });\n\n    expect(typeof result.current.isVisible).toBe(\"function\");\n  });\n\n  it(\"provides visibility context\", () => {\n    const { result } = renderHook(() => useVisibility(), {\n      wrapper: createWrapper({ test: true }),\n    });\n\n    expect(result.current.ctx.stateModel).toEqual({ test: true });\n  });\n});\n\ndescribe(\"useIsVisible\", () => {\n  it(\"returns true for undefined condition\", () => {\n    const { result } = renderHook(() => useIsVisible(undefined), {\n      wrapper: createWrapper(),\n    });\n\n    expect(result.current).toBe(true);\n  });\n\n  it(\"returns true for true condition\", () => {\n    const { result } = renderHook(() => useIsVisible(true), {\n      wrapper: createWrapper(),\n    });\n\n    expect(result.current).toBe(true);\n  });\n\n  it(\"returns false for false condition\", () => {\n    const { result } = renderHook(() => useIsVisible(false), {\n      wrapper: createWrapper(),\n    });\n\n    expect(result.current).toBe(false);\n  });\n\n  it(\"evaluates $state conditions against data\", () => {\n    const { result: trueResult } = renderHook(\n      () => useIsVisible({ $state: \"/isVisible\" }),\n      { wrapper: createWrapper({ isVisible: true }) },\n    );\n    expect(trueResult.current).toBe(true);\n\n    const { result: falseResult } = renderHook(\n      () => useIsVisible({ $state: \"/isVisible\" }),\n      { wrapper: createWrapper({ isVisible: false }) },\n    );\n    expect(falseResult.current).toBe(false);\n  });\n\n  it(\"evaluates equality conditions\", () => {\n    const { result } = renderHook(\n      () => useIsVisible({ $state: \"/count\", eq: 1 }),\n      { wrapper: createWrapper({ count: 1 }) },\n    );\n\n    expect(result.current).toBe(true);\n  });\n\n  it(\"evaluates array conditions (implicit AND)\", () => {\n    const { result } = renderHook(\n      () =>\n        useIsVisible([\n          { $state: \"/user/isAdmin\" },\n          { $state: \"/count\", eq: 5 },\n        ]),\n      { wrapper: createWrapper({ user: { isAdmin: true }, count: 5 }) },\n    );\n\n    expect(result.current).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/react/src/contexts/visibility.tsx",
    "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useContext,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Visibility context value\n */\nexport interface VisibilityContextValue {\n  /** Evaluate a visibility condition */\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  /** The underlying visibility context */\n  ctx: CoreVisibilityContext;\n}\n\nconst VisibilityContext = createContext<VisibilityContextValue | null>(null);\n\n/**\n * Props for VisibilityProvider\n */\nexport interface VisibilityProviderProps {\n  children: ReactNode;\n}\n\n/**\n * Provider for visibility evaluation\n */\nexport function VisibilityProvider({ children }: VisibilityProviderProps) {\n  const { state } = useStateStore();\n\n  const ctx: CoreVisibilityContext = useMemo(\n    () => ({\n      stateModel: state,\n    }),\n    [state],\n  );\n\n  const isVisible = useMemo(\n    () => (condition: VisibilityCondition | undefined) =>\n      evaluateVisibility(condition, ctx),\n    [ctx],\n  );\n\n  const value = useMemo<VisibilityContextValue>(\n    () => ({ isVisible, ctx }),\n    [isVisible, ctx],\n  );\n\n  return (\n    <VisibilityContext.Provider value={value}>\n      {children}\n    </VisibilityContext.Provider>\n  );\n}\n\n/**\n * Hook to access visibility evaluation\n */\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = useContext(VisibilityContext);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to check if a condition is visible\n */\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): boolean {\n  const { isVisible } = useVisibility();\n  return isVisible(condition);\n}\n"
  },
  {
    "path": "packages/react/src/dynamic-forms.test.tsx",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport React, { useState, useCallback, useMemo } from \"react\";\nimport { render, act, fireEvent, screen } from \"@testing-library/react\";\nimport type { Spec } from \"@json-render/core\";\nimport {\n  JSONUIProvider,\n  Renderer,\n  type ComponentRenderProps,\n} from \"./renderer\";\nimport { useStateStore } from \"./contexts/state\";\nimport { useFieldValidation } from \"./contexts/validation\";\nimport { useBoundProp } from \"./hooks\";\n\n// =============================================================================\n// Stub components\n// =============================================================================\n\nfunction Button({ element, emit }: ComponentRenderProps<{ label: string }>) {\n  return (\n    <button data-testid=\"btn\" onClick={() => emit(\"press\")}>\n      {element.props.label}\n    </button>\n  );\n}\n\nfunction Text({ element }: ComponentRenderProps<{ text: unknown }>) {\n  const value = element.props.text;\n  return (\n    <span data-testid=\"text\">\n      {value == null\n        ? \"\"\n        : typeof value === \"string\"\n          ? value\n          : JSON.stringify(value)}\n    </span>\n  );\n}\n\nfunction InputField({\n  element,\n  bindings,\n}: ComponentRenderProps<{\n  label?: string;\n  value?: string;\n  checks?: Array<{\n    type: string;\n    message: string;\n    args?: Record<string, unknown>;\n  }>;\n}>) {\n  const props = element.props;\n  const [boundValue, setBoundValue] = useBoundProp<string>(\n    props.value as string | undefined,\n    bindings?.value,\n  );\n  const [localValue, setLocalValue] = useState(\"\");\n  const isBound = !!bindings?.value;\n  const value = isBound ? (boundValue ?? \"\") : localValue;\n  const setValue = isBound ? setBoundValue : setLocalValue;\n\n  const hasValidation = !!(bindings?.value && props.checks?.length);\n  const config = useMemo(\n    () => (hasValidation ? { checks: props.checks ?? [] } : undefined),\n    [hasValidation, props.checks],\n  );\n  const { errors } = useFieldValidation(bindings?.value ?? \"\", config);\n\n  return (\n    <div>\n      {props.label && <label>{props.label}</label>}\n      <input\n        data-testid=\"input\"\n        value={value}\n        onChange={(e) => setValue(e.target.value)}\n      />\n      {errors.length > 0 && <span data-testid=\"input-error\">{errors[0]}</span>}\n    </div>\n  );\n}\n\nfunction SelectField({\n  element,\n  bindings,\n}: ComponentRenderProps<{ label?: string; value?: string }>) {\n  const props = element.props;\n  const [boundValue] = useBoundProp<string>(\n    props.value as string | undefined,\n    bindings?.value,\n  );\n  return <span data-testid=\"select-value\">{boundValue ?? \"\"}</span>;\n}\n\n/**\n * Select stub that mirrors the real shadcn Select validation behavior:\n * calls setValue then validate() synchronously in onValueChange.\n */\nfunction ValidatedSelect({\n  element,\n  bindings,\n  emit,\n}: ComponentRenderProps<{\n  label?: string;\n  name?: string;\n  options?: string[];\n  placeholder?: string;\n  value?: string;\n  checks?: Array<{\n    type: string;\n    message: string;\n    args?: Record<string, unknown>;\n  }>;\n  validateOn?: \"change\" | \"blur\" | \"submit\";\n}>) {\n  const props = element.props;\n  const [boundValue, setBoundValue] = useBoundProp<string>(\n    props.value as string | undefined,\n    bindings?.value,\n  );\n  const [localValue, setLocalValue] = useState(\"\");\n  const isBound = !!bindings?.value;\n  const value = isBound ? (boundValue ?? \"\") : localValue;\n  const setValue = isBound ? setBoundValue : setLocalValue;\n  const validateOn = props.validateOn ?? \"change\";\n\n  const hasValidation = !!(bindings?.value && props.checks?.length);\n  const config = useMemo(\n    () =>\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    [hasValidation, props.checks, validateOn],\n  );\n  const { errors, validate } = useFieldValidation(\n    bindings?.value ?? \"\",\n    config,\n  );\n\n  const options = props.options ?? [];\n\n  return (\n    <div>\n      {props.label && <label>{props.label}</label>}\n      <select\n        data-testid={`select-${props.name ?? \"default\"}`}\n        value={value}\n        onChange={(e) => {\n          setValue(e.target.value);\n          if (hasValidation && validateOn === \"change\") validate();\n          emit(\"change\");\n        }}\n      >\n        <option value=\"\">{props.placeholder ?? \"Select...\"}</option>\n        {options.map((opt) => (\n          <option key={opt} value={opt}>\n            {opt}\n          </option>\n        ))}\n      </select>\n      {errors.length > 0 && (\n        <span data-testid={`select-error-${props.name ?? \"default\"}`}>\n          {errors[0]}\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction StateProbe() {\n  const { state } = useStateStore();\n  return <pre data-testid=\"state-probe\">{JSON.stringify(state)}</pre>;\n}\n\nconst registry = { Button, Text, Input: InputField, Select: SelectField };\n\nfunction getState(): Record<string, unknown> {\n  return JSON.parse(screen.getByTestId(\"state-probe\").textContent!);\n}\n\n// =============================================================================\n// $computed expressions in rendering\n// =============================================================================\n\ndescribe(\"$computed expressions in rendering\", () => {\n  it(\"resolves a $computed prop using provided functions\", async () => {\n    const spec: Spec = {\n      state: { first: \"Jane\", last: \"Doe\" },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: {\n              $computed: \"fullName\",\n              args: {\n                first: { $state: \"/first\" },\n                last: { $state: \"/last\" },\n              },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    const functions = {\n      fullName: (args: Record<string, unknown>) => `${args.first} ${args.last}`,\n    };\n\n    render(\n      <JSONUIProvider\n        registry={registry}\n        initialState={spec.state}\n        functions={functions}\n      >\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>,\n    );\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"Jane Doe\");\n  });\n\n  it(\"renders gracefully when functions prop is omitted\", async () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const spec: Spec = {\n      state: {},\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: { $computed: \"missing\" },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>,\n    );\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"\");\n    warnSpy.mockRestore();\n  });\n});\n\n// =============================================================================\n// $template expressions in rendering\n// =============================================================================\n\ndescribe(\"$template expressions in rendering\", () => {\n  it(\"interpolates state values into a template string\", () => {\n    const spec: Spec = {\n      state: { user: { name: \"Alice\" }, count: 3 },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: {\n              $template: \"Hello, ${/user/name}! You have ${/count} messages.\",\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>,\n    );\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\n      \"Hello, Alice! You have 3 messages.\",\n    );\n  });\n\n  it(\"resolves missing paths to empty string\", () => {\n    const spec: Spec = {\n      state: {},\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: { $template: \"Hi ${/name}!\" },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>,\n    );\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"Hi !\");\n  });\n});\n\n// =============================================================================\n// Watchers\n// =============================================================================\n\ndescribe(\"watchers (watch field)\", () => {\n  it(\"does not fire on initial render, fires when watched state changes\", async () => {\n    const loadCities = vi.fn(async (params: Record<string, unknown>) => {\n      // no-op, just tracking the call\n    });\n\n    const spec: Spec = {\n      state: { form: { country: \"\" }, citiesLoaded: false },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Set Country\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/form/country\", value: \"US\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Select\",\n          props: { value: { $state: \"/form/country\" } },\n          watch: {\n            \"/form/country\": {\n              action: \"loadCities\",\n              params: { country: { $state: \"/form/country\" } },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    // Add watcher as a child of a wrapper so both render\n    const wrapperSpec: Spec = {\n      ...spec,\n      root: \"wrapper\",\n      elements: {\n        ...spec.elements,\n        wrapper: {\n          type: \"Button\",\n          props: { label: \"wrapper\" },\n          children: [\"main\", \"watcher\"],\n        },\n      },\n    };\n\n    // Use a Stack-like wrapper -- but since we only have Button/Text/Select\n    // stubs, we need a container. Let's add a simple Stack stub.\n    const Stack = ({\n      children,\n    }: ComponentRenderProps<Record<string, unknown>>) => {\n      return <div data-testid=\"stack\">{children}</div>;\n    };\n    const reg = { ...registry, Stack };\n\n    const stackSpec: Spec = {\n      state: { form: { country: \"\" }, citiesLoaded: false },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Set Country\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/form/country\", value: \"US\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Select\",\n          props: { value: { $state: \"/form/country\" } },\n          watch: {\n            \"/form/country\": {\n              action: \"loadCities\",\n              params: { country: { $state: \"/form/country\" } },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider\n        registry={reg}\n        initialState={stackSpec.state}\n        handlers={{ loadCities }}\n      >\n        <Renderer spec={stackSpec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    // Not called on initial render\n    expect(loadCities).not.toHaveBeenCalled();\n\n    // Change the watched state path\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    expect(loadCities).toHaveBeenCalledTimes(1);\n    expect(loadCities).toHaveBeenCalledWith(\n      expect.objectContaining({ country: \"US\" }),\n    );\n  });\n\n  it(\"fires multiple action bindings on the same watch path\", async () => {\n    const action1 = vi.fn();\n    const action2 = vi.fn();\n\n    const Stack = ({\n      children,\n    }: ComponentRenderProps<Record<string, unknown>>) => <div>{children}</div>;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { value: \"a\" },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Change\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/value\", value: \"b\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Text\",\n          props: { text: { $state: \"/value\" } },\n          watch: {\n            \"/value\": [{ action: \"action1\" }, { action: \"action2\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider\n        registry={reg}\n        initialState={spec.state}\n        handlers={{ action1, action2 }}\n      >\n        <Renderer spec={spec} registry={reg} />\n      </JSONUIProvider>,\n    );\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    expect(action1).toHaveBeenCalledTimes(1);\n    expect(action2).toHaveBeenCalledTimes(1);\n  });\n});\n\n// =============================================================================\n// validateForm action\n// =============================================================================\n\ndescribe(\"validateForm action\", () => {\n  it(\"writes { valid: false } when a required field is empty\", async () => {\n    const Stack = ({\n      children,\n    }: ComponentRenderProps<Record<string, unknown>>) => <div>{children}</div>;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { email: \"\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    // Click submit with empty email\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const state = getState();\n    expect(state.result).toEqual({\n      valid: false,\n      errors: { \"/form/email\": [\"Email is required\"] },\n    });\n  });\n\n  it(\"writes { valid: true } when all fields pass validation\", async () => {\n    const Stack = ({\n      children,\n    }: ComponentRenderProps<Record<string, unknown>>) => <div>{children}</div>;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { email: \"test@example.com\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const state = getState();\n    expect(state.result).toEqual({ valid: true, errors: {} });\n  });\n\n  it(\"defaults to /formValidation when no statePath is provided\", async () => {\n    const Stack = ({\n      children,\n    }: ComponentRenderProps<Record<string, unknown>>) => <div>{children}</div>;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { name: \"filled\" } },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"nameInput\", \"submitBtn\"],\n        },\n        nameInput: {\n          type: \"Input\",\n          props: {\n            label: \"Name\",\n            value: { $bindState: \"/form/name\" },\n            checks: [{ type: \"required\", message: \"Required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [{ action: \"validateForm\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    await act(async () => {\n      fireEvent.click(screen.getByTestId(\"btn\"));\n    });\n\n    const state = getState();\n    expect(state.formValidation).toEqual({ valid: true, errors: {} });\n  });\n});\n\n// =============================================================================\n// Select validate-on-change timing (#151)\n// =============================================================================\n\ndescribe(\"Select validate-on-change sees the new value, not the stale value\", () => {\n  const Stack = ({\n    children,\n  }: ComponentRenderProps<Record<string, unknown>>) => <div>{children}</div>;\n\n  const regWithSelect = {\n    ...registry,\n    Stack,\n    Select: ValidatedSelect,\n  };\n\n  it(\"does not show 'required' error when selecting the first value\", async () => {\n    const spec: Spec = {\n      state: { form: { country: \"\" } },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"countrySelect\"],\n        },\n        countrySelect: {\n          type: \"Select\",\n          props: {\n            label: \"Country\",\n            name: \"country\",\n            options: [\"US\", \"Canada\", \"UK\"],\n            placeholder: \"Choose a country\",\n            value: { $bindState: \"/form/country\" },\n            checks: [{ type: \"required\", message: \"Country is required\" }],\n            validateOn: \"change\",\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={regWithSelect} initialState={spec.state}>\n        <Renderer spec={spec} registry={regWithSelect} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    // Select \"US\" for the first time (from empty)\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(\"select-country\"), {\n        target: { value: \"US\" },\n      });\n    });\n\n    // The value should be set in state\n    const state = getState();\n    expect((state.form as Record<string, unknown>).country).toBe(\"US\");\n\n    // No validation error should appear -- \"US\" is non-empty\n    expect(screen.queryByTestId(\"select-error-country\")).toBeNull();\n  });\n\n  it(\"does not show 'required' error when selecting the first city after country change resets it\", async () => {\n    const spec: Spec = {\n      state: {\n        form: { country: \"US\", city: \"\" },\n        availableCities: [\"New York\", \"Chicago\"],\n      },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"citySelect\"],\n        },\n        citySelect: {\n          type: \"Select\",\n          props: {\n            label: \"City\",\n            name: \"city\",\n            options: [\"New York\", \"Chicago\"],\n            placeholder: \"Select a city\",\n            value: { $bindState: \"/form/city\" },\n            checks: [{ type: \"required\", message: \"City is required\" }],\n            validateOn: \"change\",\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(\n      <JSONUIProvider registry={regWithSelect} initialState={spec.state}>\n        <Renderer spec={spec} registry={regWithSelect} />\n        <StateProbe />\n      </JSONUIProvider>,\n    );\n\n    // Select \"New York\" for the first time (from empty)\n    await act(async () => {\n      fireEvent.change(screen.getByTestId(\"select-city\"), {\n        target: { value: \"New York\" },\n      });\n    });\n\n    const state = getState();\n    expect((state.form as Record<string, unknown>).city).toBe(\"New York\");\n\n    // No validation error should appear\n    expect(screen.queryByTestId(\"select-error-city\")).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/react/src/hooks.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { flatToTree, buildSpecFromParts, getTextFromParts } from \"./hooks\";\n\ndescribe(\"flatToTree\", () => {\n  it(\"converts array of elements to tree structure\", () => {\n    const elements = [\n      { key: \"container\", type: \"stack\", props: {}, parentKey: null },\n      {\n        key: \"text1\",\n        type: \"text\",\n        props: { content: \"Hello\" },\n        parentKey: \"container\",\n      },\n      {\n        key: \"text2\",\n        type: \"text\",\n        props: { content: \"World\" },\n        parentKey: \"container\",\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"container\");\n    expect(Object.keys(tree.elements)).toHaveLength(3);\n    expect(tree.elements[\"container\"]).toBeDefined();\n    expect(tree.elements[\"text1\"]).toBeDefined();\n    expect(tree.elements[\"text2\"]).toBeDefined();\n  });\n\n  it(\"builds parent-child relationships\", () => {\n    const elements = [\n      { key: \"root\", type: \"stack\", props: {}, parentKey: null },\n      { key: \"child1\", type: \"text\", props: {}, parentKey: \"root\" },\n      { key: \"child2\", type: \"text\", props: {}, parentKey: \"root\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"root\"].children).toHaveLength(2);\n    expect(tree.elements[\"root\"].children).toContain(\"child1\");\n    expect(tree.elements[\"root\"].children).toContain(\"child2\");\n  });\n\n  it(\"handles single root element\", () => {\n    const elements = [\n      {\n        key: \"only\",\n        type: \"text\",\n        props: { content: \"Single\" },\n        parentKey: null,\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"only\");\n    expect(Object.keys(tree.elements)).toHaveLength(1);\n  });\n\n  it(\"handles deeply nested elements\", () => {\n    const elements = [\n      { key: \"level0\", type: \"stack\", props: {}, parentKey: null },\n      { key: \"level1\", type: \"stack\", props: {}, parentKey: \"level0\" },\n      { key: \"level2\", type: \"stack\", props: {}, parentKey: \"level1\" },\n      { key: \"level3\", type: \"text\", props: {}, parentKey: \"level2\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"level0\");\n    expect(tree.elements[\"level0\"].children).toContain(\"level1\");\n    expect(tree.elements[\"level1\"].children).toContain(\"level2\");\n    expect(tree.elements[\"level2\"].children).toContain(\"level3\");\n  });\n\n  it(\"preserves element props\", () => {\n    const elements = [\n      {\n        key: \"btn\",\n        type: \"button\",\n        props: { label: \"Click me\", variant: \"primary\" },\n        parentKey: null,\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"btn\"].props).toEqual({\n      label: \"Click me\",\n      variant: \"primary\",\n    });\n  });\n\n  it(\"preserves visibility conditions\", () => {\n    const elements = [\n      {\n        key: \"conditional\",\n        type: \"text\",\n        props: {},\n        parentKey: null,\n        visible: { $state: \"/isVisible\" },\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"conditional\"].visible).toEqual({\n      $state: \"/isVisible\",\n    });\n  });\n\n  it(\"handles elements with undefined parentKey as root\", () => {\n    const elements = [\n      { key: \"root\", type: \"stack\", props: {} } as {\n        key: string;\n        type: string;\n        props: Record<string, unknown>;\n        parentKey?: string | null;\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    // Elements without parentKey should not become root (only null parentKey)\n    // This tests the edge case\n    expect(tree.elements[\"root\"]).toBeDefined();\n  });\n\n  it(\"handles empty elements array\", () => {\n    const tree = flatToTree([]);\n\n    expect(tree.root).toBe(\"\");\n    expect(Object.keys(tree.elements)).toHaveLength(0);\n  });\n\n  it(\"handles multiple children correctly\", () => {\n    const elements = [\n      { key: \"parent\", type: \"grid\", props: {}, parentKey: null },\n      { key: \"a\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"b\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"c\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"d\", type: \"card\", props: {}, parentKey: \"parent\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"parent\"].children).toHaveLength(4);\n    expect(tree.elements[\"parent\"].children).toEqual([\"a\", \"b\", \"c\", \"d\"]);\n  });\n});\n\n// =============================================================================\n// buildSpecFromParts\n// =============================================================================\n\ndescribe(\"buildSpecFromParts\", () => {\n  it(\"returns null when no data-spec parts are present\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello there\" },\n      { type: \"text\", text: \"How can I help?\" },\n    ];\n    expect(buildSpecFromParts(parts)).toBeNull();\n  });\n\n  it(\"builds a spec from patch parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Card\", props: { title: \"Hello\" }, children: [] },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"main\");\n    expect(spec!.elements.main).toEqual({\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [],\n    });\n  });\n\n  it(\"handles flat spec parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"card-1\",\n            elements: {\n              \"card-1\": { type: \"Card\", props: {}, children: [] },\n            },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"card-1\");\n    expect(spec!.elements[\"card-1\"]).toBeDefined();\n  });\n\n  it(\"ignores non-spec parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Some text\" },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      { type: \"tool-invocation\", data: { toolName: \"search\" } },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"main\");\n  });\n\n  it(\"applies patches incrementally\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Stack\", props: {}, children: [\"child\"] },\n          },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/child\",\n            value: { type: \"Text\", props: { content: \"Hi\" }, children: [] },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(Object.keys(spec!.elements)).toHaveLength(2);\n    expect(spec!.elements.child!.props.content).toBe(\"Hi\");\n  });\n\n  it(\"handles nested spec parts via nestedToFlat\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"nested\",\n          spec: {\n            type: \"Card\",\n            props: { title: \"Nested\" },\n            children: [\n              { type: \"Text\", props: { content: \"Child\" }, children: [] },\n            ],\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBeTruthy();\n    // nestedToFlat generates keys like el-0, el-1\n    const elementKeys = Object.keys(spec!.elements);\n    expect(elementKeys.length).toBe(2);\n\n    const rootEl = spec!.elements[spec!.root];\n    expect(rootEl).toBeDefined();\n    expect(rootEl!.type).toBe(\"Card\");\n    expect(rootEl!.props.title).toBe(\"Nested\");\n    expect(rootEl!.children).toHaveLength(1);\n\n    const childKey = rootEl!.children[0]!;\n    const childEl = spec!.elements[childKey];\n    expect(childEl).toBeDefined();\n    expect(childEl!.type).toBe(\"Text\");\n    expect(childEl!.props.content).toBe(\"Child\");\n  });\n\n  it(\"handles mixed patch + flat + nested parts in sequence\", () => {\n    const parts = [\n      // Start with a patch\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Stack\", props: {}, children: [] },\n          },\n        },\n      },\n      // Then a flat spec overwrites everything\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"card-1\",\n            elements: {\n              \"card-1\": {\n                type: \"Card\",\n                props: { title: \"Flat\" },\n                children: [],\n              },\n            },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    // Flat overwrites root and elements\n    expect(spec!.root).toBe(\"card-1\");\n    expect(spec!.elements[\"card-1\"]).toBeDefined();\n    expect(spec!.elements[\"card-1\"]!.type).toBe(\"Card\");\n  });\n\n  it(\"returns empty elements map from empty parts list\", () => {\n    const spec = buildSpecFromParts([]);\n    expect(spec).toBeNull();\n  });\n});\n\n// =============================================================================\n// getTextFromParts\n// =============================================================================\n\ndescribe(\"getTextFromParts\", () => {\n  it(\"extracts text from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"World\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"returns empty string when no text parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"x\" },\n        },\n      },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"\");\n  });\n\n  it(\"ignores non-text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Before\" },\n      { type: \"data-spec\", data: {} },\n      { type: \"tool-invocation\", data: {} },\n      { type: \"text\", text: \"After\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Before\\n\\nAfter\");\n  });\n\n  it(\"trims whitespace from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"  Hello  \" },\n      { type: \"text\", text: \"  World  \" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"skips empty text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"   \" },\n      { type: \"text\", text: \"World\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"ignores text parts with non-string text field\", () => {\n    const parts = [\n      { type: \"text\", text: \"Valid\" },\n      { type: \"text\", text: undefined as unknown as string },\n      { type: \"text\", text: 42 as unknown as string },\n      { type: \"text\", text: \"Also valid\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Valid\\n\\nAlso valid\");\n  });\n});\n"
  },
  {
    "path": "packages/react/src/hooks.ts",
    "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport type {\n  Spec,\n  UIElement,\n  FlatElement,\n  JsonPatch,\n  SpecDataPart,\n} from \"@json-render/core\";\nimport {\n  setByPath,\n  getByPath,\n  addByPath,\n  removeByPath,\n  createMixedStreamParser,\n  applySpecPatch,\n  nestedToFlat,\n  SPEC_DATA_PART_TYPE,\n} from \"@json-render/core\";\n\n/**\n * Token usage metadata from AI generation\n */\nexport interface TokenUsage {\n  promptTokens: number;\n  completionTokens: number;\n  totalTokens: number;\n}\n\n/**\n * Parse result for a single line -- either a patch or usage metadata\n */\ntype ParsedLine =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"usage\"; usage: TokenUsage }\n  | null;\n\n/**\n * Parse a single JSON line (patch or metadata)\n */\nfunction parseLine(line: string): ParsedLine {\n  try {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"//\")) {\n      return null;\n    }\n    const parsed = JSON.parse(trimmed);\n\n    // Check for usage metadata\n    if (parsed.__meta === \"usage\") {\n      return {\n        type: \"usage\",\n        usage: {\n          promptTokens: parsed.promptTokens ?? 0,\n          completionTokens: parsed.completionTokens ?? 0,\n          totalTokens: parsed.totalTokens ?? 0,\n        },\n      };\n    }\n\n    return { type: \"patch\", patch: parsed as JsonPatch };\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Set a value at a spec path (for add/replace operations).\n */\nfunction setSpecValue(newSpec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    const statePath = path.slice(\"/state\".length); // e.g. \"/posts\"\n    setByPath(newSpec.state as Record<string, unknown>, statePath, value);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      newSpec.elements[elementKey] = value as UIElement;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Remove a value at a spec path.\n */\nfunction removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    delete newSpec.state;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    const statePath = path.slice(\"/state\".length);\n    removeByPath(newSpec.state as Record<string, unknown>, statePath);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      const { [elementKey]: _, ...rest } = newSpec.elements;\n      newSpec.elements = rest;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Get a value at a spec path.\n */\nfunction getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  if (path === \"/state\") return spec.state;\n  if (path.startsWith(\"/state/\") && spec.state) {\n    const statePath = path.slice(\"/state\".length);\n    return getByPath(spec.state as Record<string, unknown>, statePath);\n  }\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\n/**\n * Apply an RFC 6902 JSON patch to the current spec.\n * Supports add, remove, replace, move, copy, and test operations.\n */\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\": {\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    }\n    case \"remove\": {\n      removeSpecValue(newSpec, patch.path);\n      break;\n    }\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getSpecValue(newSpec, patch.from);\n      removeSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      // test is a no-op for rendering purposes (validation only)\n      break;\n    }\n  }\n\n  return newSpec;\n}\n\n/**\n * Options for useUIStream\n */\nexport interface UseUIStreamOptions {\n  /** API endpoint */\n  api: string;\n  /** Callback when complete */\n  onComplete?: (spec: Spec) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useUIStream\n */\nexport interface UseUIStreamReturn {\n  /** Current UI spec */\n  spec: Spec | null;\n  /** Whether currently streaming */\n  isStreaming: boolean;\n  /** Error if any */\n  error: Error | null;\n  /** Token usage from the last generation */\n  usage: TokenUsage | null;\n  /** Raw JSONL lines received from the stream (JSON patch lines) */\n  rawLines: string[];\n  /** Send a prompt to generate UI */\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  /** Clear the current spec */\n  clear: () => void;\n}\n\n/**\n * Hook for streaming UI generation\n */\nexport function useUIStream({\n  api,\n  onComplete,\n  onError,\n}: UseUIStreamOptions): UseUIStreamReturn {\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const [usage, setUsage] = useState<TokenUsage | null>(null);\n  const [rawLines, setRawLines] = useState<string[]>([]);\n  const abortControllerRef = useRef<AbortController | null>(null);\n\n  // Keep refs to callbacks so `send` doesn't recreate when consumers\n  // pass inline arrow functions.\n  const onCompleteRef = useRef(onComplete);\n  onCompleteRef.current = onComplete;\n  const onErrorRef = useRef(onError);\n  onErrorRef.current = onError;\n\n  const clear = useCallback(() => {\n    setSpec(null);\n    setError(null);\n  }, []);\n\n  const send = useCallback(\n    async (prompt: string, context?: Record<string, unknown>) => {\n      // Abort any existing request\n      abortControllerRef.current?.abort();\n      abortControllerRef.current = new AbortController();\n\n      setIsStreaming(true);\n      setError(null);\n      setUsage(null);\n      setRawLines([]);\n\n      // Start with previous spec if provided, otherwise empty spec\n      const previousSpec = context?.previousSpec as Spec | undefined;\n      let currentSpec: Spec =\n        previousSpec && previousSpec.root\n          ? { ...previousSpec, elements: { ...previousSpec.elements } }\n          : { root: \"\", elements: {} };\n      setSpec(currentSpec);\n\n      try {\n        const response = await fetch(api, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            prompt,\n            context,\n            currentSpec,\n          }),\n          signal: abortControllerRef.current.signal,\n        });\n\n        if (!response.ok) {\n          // Try to parse JSON error response for better error messages\n          let errorMessage = `HTTP error: ${response.status}`;\n          try {\n            const errorData = await response.json();\n            if (errorData.message) {\n              errorMessage = errorData.message;\n            } else if (errorData.error) {\n              errorMessage = errorData.error;\n            }\n          } catch {\n            // Ignore JSON parsing errors, use default message\n          }\n          throw new Error(errorMessage);\n        }\n\n        const reader = response.body?.getReader();\n        if (!reader) {\n          throw new Error(\"No response body\");\n        }\n\n        const decoder = new TextDecoder();\n        let buffer = \"\";\n\n        while (true) {\n          const { done, value } = await reader.read();\n          if (done) break;\n\n          buffer += decoder.decode(value, { stream: true });\n\n          // Process complete lines\n          const lines = buffer.split(\"\\n\");\n          buffer = lines.pop() ?? \"\";\n\n          for (const line of lines) {\n            const trimmed = line.trim();\n            if (!trimmed) continue;\n            const result = parseLine(trimmed);\n            if (!result) continue;\n            if (result.type === \"usage\") {\n              setUsage(result.usage);\n            } else {\n              setRawLines((prev) => [...prev, trimmed]);\n              currentSpec = applyPatch(currentSpec, result.patch);\n              setSpec({ ...currentSpec });\n            }\n          }\n        }\n\n        // Process any remaining buffer\n        if (buffer.trim()) {\n          const trimmed = buffer.trim();\n          const result = parseLine(trimmed);\n          if (result) {\n            if (result.type === \"usage\") {\n              setUsage(result.usage);\n            } else {\n              setRawLines((prev) => [...prev, trimmed]);\n              currentSpec = applyPatch(currentSpec, result.patch);\n              setSpec({ ...currentSpec });\n            }\n          }\n        }\n\n        onCompleteRef.current?.(currentSpec);\n      } catch (err) {\n        if ((err as Error).name === \"AbortError\") {\n          return;\n        }\n        const error = err instanceof Error ? err : new Error(String(err));\n        setError(error);\n        onErrorRef.current?.(error);\n      } finally {\n        setIsStreaming(false);\n      }\n    },\n    [api],\n  );\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  return {\n    spec,\n    isStreaming,\n    error,\n    usage,\n    rawLines,\n    send,\n    clear,\n  };\n}\n\n/**\n * Convert a flat element list to a Spec.\n * Input elements use key/parentKey to establish identity and relationships.\n * Output spec uses the map-based format where key is the map entry key\n * and parent-child relationships are expressed through children arrays.\n */\nexport function flatToTree(elements: FlatElement[]): Spec {\n  const elementMap: Record<string, UIElement> = {};\n  let root = \"\";\n\n  // First pass: add all elements to map\n  for (const element of elements) {\n    elementMap[element.key] = {\n      type: element.type,\n      props: element.props,\n      children: [],\n      visible: element.visible,\n    };\n  }\n\n  // Second pass: build parent-child relationships\n  for (const element of elements) {\n    if (element.parentKey) {\n      const parent = elementMap[element.parentKey];\n      if (parent) {\n        if (!parent.children) {\n          parent.children = [];\n        }\n        parent.children.push(element.key);\n      }\n    } else {\n      root = element.key;\n    }\n  }\n\n  return { root, elements: elementMap };\n}\n\n// =============================================================================\n// useBoundProp — Two-way binding helper for $bindState/$bindItem expressions\n// =============================================================================\n\n/**\n * Hook for two-way bound props. Returns `[value, setValue]` where:\n *\n * - `value` is the already-resolved prop value (passed through from render props)\n * - `setValue` writes back to the bound state path (no-op if not bound)\n *\n * Designed to work with the `bindings` map that the renderer provides when\n * a prop uses `{ $bindState: \"/path\" }` or `{ $bindItem: \"field\" }`.\n *\n * @example\n * ```tsx\n * import { useBoundProp } from \"@json-render/react\";\n *\n * const Input: ComponentRenderer = ({ props, bindings }) => {\n *   const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);\n *   return <input value={value ?? \"\"} onChange={(e) => setValue(e.target.value)} />;\n * };\n * ```\n */\nexport function useBoundProp<T>(\n  propValue: T | undefined,\n  bindingPath: string | undefined,\n): [T | undefined, (value: T) => void] {\n  // Import useStateStore lazily to avoid circular dependency issues.\n  // The hook is always called inside a StateProvider so this is safe.\n  const { set } = useStateStoreFromContext();\n  const setValue = useCallback(\n    (value: T) => {\n      if (bindingPath) set(bindingPath, value);\n    },\n    [bindingPath, set],\n  );\n  return [propValue, setValue];\n}\n\n// Re-export useStateStore access for useBoundProp without circular import\nimport { useStateStore as useStateStoreFromContext } from \"./contexts/state\";\n\n// =============================================================================\n// buildSpecFromParts — Derive Spec from AI SDK data parts\n// =============================================================================\n\n/**\n * A single part from the AI SDK's `message.parts` array. This is a minimal\n * structural type so that library helpers do not depend on the AI SDK.\n * Fields are optional because different part types carry different data:\n * - Text parts have `text`\n * - Data parts have `data`\n */\nexport interface DataPart {\n  type: string;\n  text?: string;\n  data?: unknown;\n}\n\n/**\n * Build a `Spec` by replaying all spec data parts from a message's\n * parts array. Returns `null` if no spec data parts are present.\n *\n * This function is designed to work with the AI SDK's `UIMessage.parts` array.\n * It picks out parts whose `type` is {@link SPEC_DATA_PART_TYPE} and processes them based\n * on the payload's `type` discriminator:\n *\n * - `\"patch\"`: Applies the JSON Patch operation incrementally via `applySpecPatch`.\n * - `\"flat\"`: Replaces the spec with the complete flat spec.\n * - `\"nested\"`: Assigns the nested spec directly (future: nested-to-flat conversion).\n *\n * The function has no AI SDK dependency — it operates on a generic array of\n * `{ type: string; data: unknown }` objects.\n *\n * @example\n * ```tsx\n * const spec = buildSpecFromParts(message.parts);\n * if (spec) {\n *   return <MyRenderer spec={spec} />;\n * }\n * ```\n */\n/**\n * Type guard that validates a data part payload looks like a valid\n * {@link SpecDataPart} before we cast it. Returns `false` (and the\n * part is silently skipped) for malformed payloads.\n */\nfunction isSpecDataPart(data: unknown): data is SpecDataPart {\n  if (typeof data !== \"object\" || data === null) return false;\n  const obj = data as Record<string, unknown>;\n  switch (obj.type) {\n    case \"patch\":\n      return typeof obj.patch === \"object\" && obj.patch !== null;\n    case \"flat\":\n    case \"nested\":\n      return typeof obj.spec === \"object\" && obj.spec !== null;\n    default:\n      return false;\n  }\n}\n\nexport function buildSpecFromParts(parts: DataPart[]): Spec | null {\n  const spec: Spec = { root: \"\", elements: {} };\n  let hasSpec = false;\n\n  for (const part of parts) {\n    if (part.type === SPEC_DATA_PART_TYPE) {\n      if (!isSpecDataPart(part.data)) continue;\n      const payload = part.data;\n      if (payload.type === \"patch\") {\n        hasSpec = true;\n        applySpecPatch(spec, payload.patch);\n      } else if (payload.type === \"flat\") {\n        hasSpec = true;\n        Object.assign(spec, payload.spec);\n      } else if (payload.type === \"nested\") {\n        hasSpec = true;\n        const flat = nestedToFlat(payload.spec);\n        Object.assign(spec, flat);\n      }\n    }\n  }\n\n  return hasSpec ? spec : null;\n}\n\n/**\n * Extract and join all text content from a message's parts array.\n *\n * Filters for parts with `type === \"text\"`, trims each one, and joins them\n * with double newlines so that text from separate agent steps renders as\n * distinct paragraphs in markdown.\n *\n * Has no AI SDK dependency — operates on a generic `DataPart[]`.\n *\n * @example\n * ```tsx\n * const text = getTextFromParts(message.parts);\n * if (text) {\n *   return <Streamdown>{text}</Streamdown>;\n * }\n * ```\n */\nexport function getTextFromParts(parts: DataPart[]): string {\n  return parts\n    .filter(\n      (p): p is DataPart & { text: string } =>\n        p.type === \"text\" && typeof p.text === \"string\",\n    )\n    .map((p) => p.text.trim())\n    .filter(Boolean)\n    .join(\"\\n\\n\");\n}\n\n// =============================================================================\n// useJsonRenderMessage — extract spec + text from message parts\n// =============================================================================\n\n/**\n * Hook that extracts both the json-render spec and text content from a\n * message's parts array. Combines `buildSpecFromParts` and `getTextFromParts`\n * into a single call with memoized results.\n *\n * **Memoization behavior:** Results are recomputed only when the `parts` array\n * reference changes **and** either the length differs or the last element is a\n * different object. This is optimized for the typical AI SDK streaming pattern\n * where parts are appended incrementally. Mid-array edits (e.g. replacing an\n * earlier part without appending) may not trigger recomputation. If you need to\n * force a recompute after such edits, pass a new array reference with a\n * different last element.\n *\n * @example\n * ```tsx\n * import { useJsonRenderMessage } from \"@json-render/react\";\n *\n * function MessageBubble({ message }) {\n *   const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);\n *\n *   return (\n *     <div>\n *       {text && <Markdown>{text}</Markdown>}\n *       {hasSpec && <MyRenderer spec={spec} />}\n *     </div>\n *   );\n * }\n * ```\n */\nexport function useJsonRenderMessage(parts: DataPart[]) {\n  const prevPartsRef = useRef<DataPart[]>([]);\n  const prevResultRef = useRef<{ spec: Spec | null; text: string }>({\n    spec: null,\n    text: \"\",\n  });\n\n  // Recompute only when parts actually change (by length + last element identity).\n  // AI SDK typically appends to the parts array during streaming, so checking\n  // length and the last element covers both \"new array reference with same\n  // content\" (no recompute) and \"new part appended\" (recompute).\n  const partsChanged =\n    parts !== prevPartsRef.current &&\n    (parts.length !== prevPartsRef.current.length ||\n      parts[parts.length - 1] !==\n        prevPartsRef.current[prevPartsRef.current.length - 1]);\n\n  if (partsChanged || prevPartsRef.current.length === 0) {\n    prevPartsRef.current = parts;\n    prevResultRef.current = {\n      spec: buildSpecFromParts(parts),\n      text: getTextFromParts(parts),\n    };\n  }\n\n  const { spec, text } = prevResultRef.current;\n  const hasSpec = spec !== null && Object.keys(spec.elements || {}).length > 0;\n  return { spec, text, hasSpec };\n}\n\n// =============================================================================\n// useChatUI — Chat + GenUI hook\n// =============================================================================\n\n/**\n * A single message in the chat, which may contain text, a rendered UI spec, or both.\n */\nexport interface ChatMessage {\n  /** Unique message ID */\n  id: string;\n  /** Who sent this message */\n  role: \"user\" | \"assistant\";\n  /** Text content (conversational prose) */\n  text: string;\n  /** json-render Spec built from JSONL patches (null if no UI was generated) */\n  spec: Spec | null;\n}\n\n/**\n * Options for useChatUI\n */\nexport interface UseChatUIOptions {\n  /** API endpoint that accepts `{ messages: Array<{ role, content }> }` and returns a text stream */\n  api: string;\n  /** Callback when streaming completes for a message */\n  onComplete?: (message: ChatMessage) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useChatUI\n */\nexport interface UseChatUIReturn {\n  /** All messages in the conversation */\n  messages: ChatMessage[];\n  /** Whether currently streaming an assistant response */\n  isStreaming: boolean;\n  /** Error from the last request, if any */\n  error: Error | null;\n  /** Send a user message */\n  send: (text: string) => Promise<void>;\n  /** Clear all messages and reset the conversation */\n  clear: () => void;\n}\n\nlet chatMessageIdCounter = 0;\nfunction generateChatId(): string {\n  if (\n    typeof crypto !== \"undefined\" &&\n    typeof crypto.randomUUID === \"function\"\n  ) {\n    return crypto.randomUUID();\n  }\n  chatMessageIdCounter += 1;\n  return `msg-${Date.now()}-${chatMessageIdCounter}`;\n}\n\n/**\n * Hook for chat + GenUI experiences.\n *\n * Manages a multi-turn conversation where each assistant message can contain\n * both conversational text and a json-render UI spec. The hook sends the full\n * message history to the API endpoint, reads the streamed response, and\n * separates text lines from JSONL patch lines using `createMixedStreamParser`.\n *\n * @example\n * ```tsx\n * const { messages, isStreaming, send, clear } = useChatUI({\n *   api: \"/api/chat\",\n * });\n *\n * // Send a message\n * await send(\"Compare weather in NYC and Tokyo\");\n *\n * // Render messages\n * {messages.map((msg) => (\n *   <div key={msg.id}>\n *     {msg.text && <p>{msg.text}</p>}\n *     {msg.spec && <MyRenderer spec={msg.spec} />}\n *   </div>\n * ))}\n * ```\n */\nexport function useChatUI({\n  api,\n  onComplete,\n  onError,\n}: UseChatUIOptions): UseChatUIReturn {\n  const [messages, setMessages] = useState<ChatMessage[]>([]);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const abortControllerRef = useRef<AbortController | null>(null);\n  // Keep a ref to the latest messages so `send` always reads the\n  // current history, avoiding stale closure issues.\n  const messagesRef = useRef(messages);\n  messagesRef.current = messages;\n  // Keep refs to callbacks so `send` doesn't recreate when consumers\n  // pass inline arrow functions.\n  const onCompleteRef = useRef(onComplete);\n  onCompleteRef.current = onComplete;\n  const onErrorRef = useRef(onError);\n  onErrorRef.current = onError;\n\n  const clear = useCallback(() => {\n    setMessages([]);\n    setError(null);\n  }, []);\n\n  const send = useCallback(\n    async (text: string) => {\n      if (!text.trim()) return;\n\n      // Abort any existing request\n      abortControllerRef.current?.abort();\n      abortControllerRef.current = new AbortController();\n\n      const userMessage: ChatMessage = {\n        id: generateChatId(),\n        role: \"user\",\n        text: text.trim(),\n        spec: null,\n      };\n\n      const assistantId = generateChatId();\n      const assistantMessage: ChatMessage = {\n        id: assistantId,\n        role: \"assistant\",\n        text: \"\",\n        spec: null,\n      };\n\n      // Append user message and empty assistant placeholder\n      setMessages((prev) => [...prev, userMessage, assistantMessage]);\n      setIsStreaming(true);\n      setError(null);\n\n      // Build messages array for the API (full conversation history + new message).\n      // Read from ref to always get the latest messages (avoids stale closure).\n      const historyForApi = [\n        ...messagesRef.current.map((m) => ({\n          role: m.role,\n          content: m.text,\n        })),\n        { role: \"user\" as const, content: text.trim() },\n      ];\n\n      // Mutable state for accumulating the assistant response\n      let accumulatedText = \"\";\n      let currentSpec: Spec = { root: \"\", elements: {} };\n      let hasSpec = false;\n\n      try {\n        const response = await fetch(api, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ messages: historyForApi }),\n          signal: abortControllerRef.current.signal,\n        });\n\n        if (!response.ok) {\n          let errorMessage = `HTTP error: ${response.status}`;\n          try {\n            const errorData = await response.json();\n            if (errorData.message) {\n              errorMessage = errorData.message;\n            } else if (errorData.error) {\n              errorMessage = errorData.error;\n            }\n          } catch {\n            // Ignore JSON parsing errors\n          }\n          throw new Error(errorMessage);\n        }\n\n        const reader = response.body?.getReader();\n        if (!reader) {\n          throw new Error(\"No response body\");\n        }\n\n        const decoder = new TextDecoder();\n\n        // Use createMixedStreamParser to classify lines\n        const parser = createMixedStreamParser({\n          onPatch(patch) {\n            hasSpec = true;\n            applySpecPatch(currentSpec, patch);\n            setMessages((prev) =>\n              prev.map((m) =>\n                m.id === assistantId\n                  ? {\n                      ...m,\n                      spec: {\n                        root: currentSpec.root,\n                        elements: { ...currentSpec.elements },\n                        ...(currentSpec.state\n                          ? { state: { ...currentSpec.state } }\n                          : {}),\n                      },\n                    }\n                  : m,\n              ),\n            );\n          },\n          onText(line) {\n            accumulatedText += (accumulatedText ? \"\\n\" : \"\") + line;\n            setMessages((prev) =>\n              prev.map((m) =>\n                m.id === assistantId ? { ...m, text: accumulatedText } : m,\n              ),\n            );\n          },\n        });\n\n        while (true) {\n          const { done, value } = await reader.read();\n          if (done) break;\n          parser.push(decoder.decode(value, { stream: true }));\n        }\n        parser.flush();\n\n        // Build final message for onComplete callback\n        const finalMessage: ChatMessage = {\n          id: assistantId,\n          role: \"assistant\",\n          text: accumulatedText,\n          spec: hasSpec\n            ? {\n                root: currentSpec.root,\n                elements: { ...currentSpec.elements },\n                ...(currentSpec.state\n                  ? { state: { ...currentSpec.state } }\n                  : {}),\n              }\n            : null,\n        };\n        onCompleteRef.current?.(finalMessage);\n      } catch (err) {\n        if ((err as Error).name === \"AbortError\") {\n          return;\n        }\n        const resolvedError =\n          err instanceof Error ? err : new Error(String(err));\n        setError(resolvedError);\n        // Remove empty assistant message on error\n        setMessages((prev) =>\n          prev.filter((m) => m.id !== assistantId || m.text.length > 0),\n        );\n        onErrorRef.current?.(resolvedError);\n      } finally {\n        setIsStreaming(false);\n      }\n    },\n    [api],\n  );\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  return {\n    messages,\n    isStreaming,\n    error,\n    send,\n    clear,\n  };\n}\n"
  },
  {
    "path": "packages/react/src/index.ts",
    "content": "// Contexts\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./contexts/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n  type VisibilityProviderProps,\n} from \"./contexts/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./contexts/actions\";\n\nexport {\n  ValidationProvider,\n  useValidation,\n  useOptionalValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./contexts/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/repeat-scope\";\n\n// Schema (React's spec format)\nexport {\n  schema,\n  type ReactSchema,\n  type ReactSpec,\n  // Backward compatibility\n  elementTreeSchema,\n  type ElementTreeSchema,\n  type ElementTreeSpec,\n} from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec, StateStore } from \"@json-render/core\";\nexport { createStateStore } from \"@json-render/core\";\n\n// Catalog-aware types for React\nexport type {\n  EventHandle,\n  BaseComponentProps,\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n  ActionFn,\n  Actions,\n} from \"./catalog-types\";\n\n// Renderer\nexport {\n  // Registry\n  defineRegistry,\n  type DefineRegistryResult,\n  // createRenderer (higher-level, includes providers)\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  // Low-level\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRenderer,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n} from \"./renderer\";\n\n// Hooks\nexport {\n  useUIStream,\n  useChatUI,\n  useBoundProp,\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n  useJsonRenderMessage,\n  type UseUIStreamOptions,\n  type UseUIStreamReturn,\n  type UseChatUIOptions,\n  type UseChatUIReturn,\n  type ChatMessage,\n  type DataPart,\n  type TokenUsage,\n} from \"./hooks\";\n"
  },
  {
    "path": "packages/react/src/renderer.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport React from \"react\";\nimport { Renderer } from \"./renderer\";\n\ndescribe(\"Renderer\", () => {\n  it(\"renders null for null spec\", () => {\n    const element = React.createElement(Renderer, {\n      spec: null,\n      registry: {},\n    });\n    expect(element).toBeDefined();\n    expect(element.props.spec).toBeNull();\n  });\n\n  it(\"renders null for spec without root\", () => {\n    const element = React.createElement(Renderer, {\n      spec: { root: \"\", elements: {} },\n      registry: {},\n    });\n    expect(element).toBeDefined();\n  });\n\n  it(\"accepts loading prop\", () => {\n    const element = React.createElement(Renderer, {\n      spec: null,\n      registry: {},\n      loading: true,\n    });\n    expect(element.props.loading).toBe(true);\n  });\n\n  it(\"accepts fallback prop\", () => {\n    const Fallback = () =>\n      React.createElement(\"div\", null, \"Unknown component\");\n\n    const element = React.createElement(Renderer, {\n      spec: null,\n      registry: {},\n      fallback: Fallback,\n    });\n    expect(element.props.fallback).toBe(Fallback);\n  });\n});\n"
  },
  {
    "path": "packages/react/src/renderer.tsx",
    "content": "\"use client\";\n\nimport React, {\n  type ComponentType,\n  type ErrorInfo,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n} from \"react\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  SchemaDefinition,\n  StateStore,\n  ComputedFunction,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport type {\n  Components,\n  Actions,\n  ActionFn,\n  SetState,\n  StateModel,\n  CatalogHasActions,\n  EventHandle,\n} from \"./catalog-types\";\nimport { useIsVisible, useVisibility } from \"./contexts/visibility\";\nimport { useActions } from \"./contexts/actions\";\nimport { useStateStore } from \"./contexts/state\";\nimport { StateProvider } from \"./contexts/state\";\nimport { VisibilityProvider } from \"./contexts/visibility\";\nimport { ActionProvider } from \"./contexts/actions\";\nimport { ValidationProvider } from \"./contexts/validation\";\nimport { ConfirmDialog } from \"./contexts/actions\";\nimport { RepeatScopeProvider, useRepeatScope } from \"./contexts/repeat-scope\";\n\n/**\n * Props passed to component renderers\n */\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  /** The element being rendered */\n  element: UIElement<string, P>;\n  /** Rendered children */\n  children?: ReactNode;\n  /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. Always provided by the renderer. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata (shouldPreventDefault, bound). Use when you need to inspect event bindings. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   * Only present when at least one prop uses `{ $bindState: \"...\" }` or `{ $bindItem: \"...\" }`.\n   */\n  bindings?: Record<string, string>;\n  /** Whether the parent is loading */\n  loading?: boolean;\n}\n\n/**\n * Component renderer type\n */\nexport type ComponentRenderer<P = Record<string, unknown>> = ComponentType<\n  ComponentRenderProps<P>\n>;\n\n/**\n * Registry of component renderers\n */\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\n/**\n * Props for the Renderer component\n */\nexport interface RendererProps {\n  /** The UI spec to render */\n  spec: Spec | null;\n  /** Component registry */\n  registry: ComponentRegistry;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n// ---------------------------------------------------------------------------\n// ElementErrorBoundary – catches rendering errors in individual elements so\n// a single bad component never crashes the whole page.\n// ---------------------------------------------------------------------------\n\ninterface ElementErrorBoundaryProps {\n  elementType: string;\n  children: ReactNode;\n}\n\ninterface ElementErrorBoundaryState {\n  hasError: boolean;\n}\n\nclass ElementErrorBoundary extends React.Component<\n  ElementErrorBoundaryProps,\n  ElementErrorBoundaryState\n> {\n  constructor(props: ElementErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ElementErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    console.error(\n      `[json-render] Rendering error in <${this.props.elementType}>:`,\n      error,\n      info.componentStack,\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // Render nothing – the element silently disappears rather than\n      // crashing the entire application.\n      return null;\n    }\n    return this.props.children;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// FunctionsContext – provides $computed functions to the element tree\n// ---------------------------------------------------------------------------\n\nconst EMPTY_FUNCTIONS: Record<string, ComputedFunction> = {};\n\nconst FunctionsContext =\n  React.createContext<Record<string, ComputedFunction>>(EMPTY_FUNCTIONS);\n\nfunction useFunctions(): Record<string, ComputedFunction> {\n  return React.useContext(FunctionsContext);\n}\n\ninterface ElementRendererProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Element renderer component.\n * Memoized to prevent re-rendering all repeat children when state changes.\n */\nconst ElementRenderer = React.memo(function ElementRenderer({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: ElementRendererProps) {\n  const repeatScope = useRepeatScope();\n  const { ctx } = useVisibility();\n  const { execute } = useActions();\n  const { getSnapshot, state: watchState } = useStateStore();\n  const functions = useFunctions();\n\n  // Build context with repeat scope and $computed functions\n  const fullCtx: PropResolutionContext = useMemo(() => {\n    const base: PropResolutionContext = repeatScope\n      ? {\n          ...ctx,\n          repeatItem: repeatScope.item,\n          repeatIndex: repeatScope.index,\n          repeatBasePath: repeatScope.basePath,\n        }\n      : { ...ctx };\n    base.functions = functions;\n    return base;\n  }, [ctx, repeatScope, functions]);\n\n  // Evaluate visibility (now supports $item/$index inside repeat scopes)\n  const isVisible =\n    element.visible === undefined\n      ? true\n      : evaluateVisibility(element.visible, fullCtx);\n\n  // Create emit function that resolves events to action bindings.\n  // Must be called before any early return to satisfy Rules of Hooks.\n  const onBindings = element.on;\n  const emit = useCallback(\n    async (eventName: string) => {\n      const binding = onBindings?.[eventName];\n      if (!binding) return;\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      for (const b of actionBindings) {\n        if (!b.params) {\n          await execute(b);\n          continue;\n        }\n        // Build a fresh context with live store state so that $state\n        // references in later actions see mutations from earlier ones.\n        const liveCtx: PropResolutionContext = {\n          ...fullCtx,\n          stateModel: getSnapshot(),\n        };\n        const resolved: Record<string, unknown> = {};\n        for (const [key, val] of Object.entries(b.params)) {\n          resolved[key] = resolveActionParam(val, liveCtx);\n        }\n        await execute({ ...b, params: resolved });\n      }\n    },\n    [onBindings, execute, fullCtx, getSnapshot],\n  );\n\n  // Create on() function that returns an EventHandle with metadata for a specific event.\n  const on = useCallback(\n    (eventName: string): EventHandle => {\n      const binding = onBindings?.[eventName];\n      if (!binding) {\n        return { emit: () => {}, shouldPreventDefault: false, bound: false };\n      }\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);\n      return {\n        emit: () => emit(eventName),\n        shouldPreventDefault,\n        bound: true,\n      };\n    },\n    [onBindings, emit],\n  );\n\n  // Watch effect: fire actions when watched state paths change.\n  // Must be called before any early return to satisfy Rules of Hooks.\n  //\n  // Two refs serve distinct roles:\n  // - `stableWatchRef` (useMemo): holds the last emitted values object so we\n  //   can return the same reference when watched values haven't changed,\n  //   preventing the downstream useEffect from firing on unrelated state updates.\n  // - `prevWatchValues` (useEffect): tracks the previous watched-values snapshot\n  //   for change detection. Starts as `null` to skip the initial mount.\n  const watchConfig = element.watch;\n  const prevWatchValues = useRef<Record<string, unknown> | null>(null);\n  const stableWatchRef = useRef<Record<string, unknown> | undefined>(undefined);\n\n  const watchedValues = useMemo(() => {\n    if (!watchConfig) return undefined;\n    const values: Record<string, unknown> = {};\n    for (const path of Object.keys(watchConfig)) {\n      values[path] = getByPath(watchState, path);\n    }\n    const prev = stableWatchRef.current;\n    if (prev) {\n      const keys = Object.keys(values);\n      if (\n        keys.length === Object.keys(prev).length &&\n        keys.every((k) => values[k] === prev[k])\n      ) {\n        return prev;\n      }\n    }\n    stableWatchRef.current = values;\n    return values;\n  }, [watchConfig, watchState]);\n\n  useEffect(() => {\n    if (!watchConfig || !watchedValues) return;\n    const paths = Object.keys(watchConfig);\n    if (paths.length === 0) return;\n\n    const prev = prevWatchValues.current;\n    prevWatchValues.current = watchedValues;\n\n    // Skip the initial mount — only fire on changes\n    if (prev === null) return;\n\n    let cancelled = false;\n    void (async () => {\n      for (const path of paths) {\n        if (cancelled) break;\n        if (watchedValues[path] !== prev[path]) {\n          const binding = watchConfig[path];\n          if (!binding) continue;\n          const bindings = Array.isArray(binding) ? binding : [binding];\n          for (const b of bindings) {\n            if (cancelled) break;\n            if (!b.params) {\n              await execute(b);\n              if (cancelled) break;\n              continue;\n            }\n            const liveCtx: PropResolutionContext = {\n              ...fullCtx,\n              stateModel: getSnapshot(),\n            };\n            const resolved: Record<string, unknown> = {};\n            for (const [key, val] of Object.entries(b.params)) {\n              resolved[key] = resolveActionParam(val, liveCtx);\n            }\n            await execute({ ...b, params: resolved });\n            if (cancelled) break;\n          }\n        }\n      }\n    })().catch(console.error);\n\n    return () => {\n      cancelled = true;\n    };\n  }, [watchConfig, watchedValues, execute, fullCtx, getSnapshot]);\n\n  // Don't render if not visible\n  if (!isVisible) {\n    return null;\n  }\n\n  // Resolve $bindState/$bindItem expressions → bindings map (prop name → state path)\n  const rawProps = element.props as Record<string, unknown>;\n  const elementBindings = resolveBindings(rawProps, fullCtx);\n\n  // Resolve dynamic prop expressions ($state, $item, $index, $bindState, $bindItem, $cond/$then/$else)\n  const resolvedProps = resolveElementProps(rawProps, fullCtx);\n\n  const resolvedElement =\n    resolvedProps !== element.props\n      ? { ...element, props: resolvedProps }\n      : element;\n\n  // Get the component renderer\n  const Component = registry[resolvedElement.type] ?? fallback;\n\n  if (!Component) {\n    console.warn(`No renderer for component type: ${resolvedElement.type}`);\n    return null;\n  }\n\n  // ---- Render children (with repeat support) ----\n  const children = resolvedElement.repeat ? (\n    <RepeatChildren\n      element={resolvedElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  ) : (\n    resolvedElement.children?.map((childKey) => {\n      const childElement = spec.elements[childKey];\n      if (!childElement) {\n        if (!loading) {\n          console.warn(\n            `[json-render] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\". This element will not render.`,\n          );\n        }\n        return null;\n      }\n      return (\n        <ElementRenderer\n          key={childKey}\n          element={childElement}\n          spec={spec}\n          registry={registry}\n          loading={loading}\n          fallback={fallback}\n        />\n      );\n    })\n  );\n\n  return (\n    <ElementErrorBoundary elementType={resolvedElement.type}>\n      <Component\n        element={resolvedElement}\n        emit={emit}\n        on={on}\n        bindings={elementBindings}\n        loading={loading}\n      >\n        {children}\n      </Component>\n    </ElementErrorBoundary>\n  );\n});\n\n// ---------------------------------------------------------------------------\n// RepeatChildren -- renders child elements once per item in a state array.\n// Used when an element has a `repeat` field.\n// ---------------------------------------------------------------------------\n\nfunction RepeatChildren({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}) {\n  const { state } = useStateStore();\n  const repeat = element.repeat!;\n  const statePath = repeat.statePath;\n\n  const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];\n\n  return (\n    <>\n      {items.map((itemValue, index) => {\n        // Use a stable key: prefer key field, fall back to index\n        const key =\n          repeat.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String(\n                (itemValue as Record<string, unknown>)[repeat.key] ?? index,\n              )\n            : String(index);\n\n        return (\n          <RepeatScopeProvider\n            key={key}\n            item={itemValue}\n            index={index}\n            basePath={`${statePath}/${index}`}\n          >\n            {element.children?.map((childKey) => {\n              const childElement = spec.elements[childKey];\n              if (!childElement) {\n                if (!loading) {\n                  console.warn(\n                    `[json-render] Missing element \"${childKey}\" referenced as child of \"${element.type}\" (repeat). This element will not render.`,\n                  );\n                }\n                return null;\n              }\n              return (\n                <ElementRenderer\n                  key={childKey}\n                  element={childElement}\n                  spec={spec}\n                  registry={registry}\n                  loading={loading}\n                  fallback={fallback}\n                />\n              );\n            })}\n          </RepeatScopeProvider>\n        );\n      })}\n    </>\n  );\n}\n\n/**\n * Main renderer component\n */\nexport function Renderer({ spec, registry, loading, fallback }: RendererProps) {\n  if (!spec || !spec.root) {\n    return null;\n  }\n\n  const rootElement = spec.elements[spec.root];\n  if (!rootElement) {\n    return null;\n  }\n\n  return (\n    <ElementRenderer\n      element={rootElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  );\n}\n\n/**\n * Props for JSONUIProvider\n */\nexport interface JSONUIProviderProps {\n  /** Component registry */\n  registry: ComponentRegistry;\n  /**\n   * External store (controlled mode). When provided, `initialState` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** Initial state model (uncontrolled mode) */\n  initialState?: Record<string, unknown>;\n  /** Action handlers */\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  /** Navigation function */\n  navigate?: (path: string) => void;\n  /** Custom validation functions */\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  children: ReactNode;\n}\n\n/**\n * Combined provider for all JSONUI contexts\n */\nexport function JSONUIProvider({\n  registry,\n  store,\n  initialState,\n  handlers,\n  navigate,\n  validationFunctions,\n  functions,\n  onStateChange,\n  children,\n}: JSONUIProviderProps) {\n  return (\n    <StateProvider\n      store={store}\n      initialState={initialState}\n      onStateChange={onStateChange}\n    >\n      <VisibilityProvider>\n        <ValidationProvider customFunctions={validationFunctions}>\n          <ActionProvider handlers={handlers} navigate={navigate}>\n            <FunctionsContext.Provider value={functions ?? EMPTY_FUNCTIONS}>\n              {children}\n              <ConfirmationDialogManager />\n            </FunctionsContext.Provider>\n          </ActionProvider>\n        </ValidationProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n/**\n * Renders the confirmation dialog when needed\n */\nfunction ConfirmationDialogManager() {\n  const { pendingConfirmation, confirm, cancel } = useActions();\n\n  if (!pendingConfirmation?.action.confirm) {\n    return null;\n  }\n\n  return (\n    <ConfirmDialog\n      confirm={pendingConfirmation.action.confirm}\n      onConfirm={confirm}\n      onCancel={cancel}\n    />\n  );\n}\n\n// ============================================================================\n// defineRegistry\n// ============================================================================\n\n/**\n * Result returned by defineRegistry\n */\nexport interface DefineRegistryResult {\n  /** Component registry for `<Renderer registry={...} />` */\n  registry: ComponentRegistry;\n  /**\n   * Create ActionProvider-compatible handlers.\n   * Accepts getter functions so handlers always read the latest state/setState\n   * (e.g. from React refs).\n   */\n  handlers: (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;\n  /**\n   * Execute an action by name imperatively\n   * (for use outside the React tree, e.g. initial state loading).\n   */\n  executeAction: (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state?: StateModel,\n  ) => Promise<void>;\n}\n\n/**\n * Options for defineRegistry.\n *\n * When the catalog declares actions, the `actions` field is required.\n * When the catalog has no actions (or `actions: {}`), the field is optional.\n */\ntype DefineRegistryOptions<C extends Catalog> = {\n  components?: Components<C>;\n} & (CatalogHasActions<C> extends true\n  ? { actions: Actions<C> }\n  : { actions?: Actions<C> });\n\n/**\n * Create a registry from a catalog with components and/or actions.\n *\n * When the catalog declares actions, the `actions` field is required.\n *\n * @example\n * ```tsx\n * // Components only (catalog has no actions)\n * const { registry } = defineRegistry(catalog, {\n *   components: {\n *     Card: ({ props, children }) => (\n *       <div className=\"card\">{props.title}{children}</div>\n *     ),\n *   },\n * });\n *\n * // Both (catalog declares actions)\n * const { registry, handlers, executeAction } = defineRegistry(catalog, {\n *   components: { ... },\n *   actions: { ... },\n * });\n * ```\n */\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: DefineRegistryOptions<C>,\n): DefineRegistryResult {\n  // Build component registry\n  const registry: ComponentRegistry = {};\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = ({\n        element,\n        children,\n        emit,\n        on,\n        bindings,\n        loading,\n      }: ComponentRenderProps) => {\n        return (componentFn as DefineRegistryComponentFn)({\n          props: element.props,\n          children,\n          emit,\n          on,\n          bindings,\n          loading,\n        });\n      };\n    }\n  }\n\n  // Build action helpers\n  const actionMap = options.actions\n    ? (Object.entries(options.actions) as Array<\n        [string, DefineRegistryActionFn]\n      >)\n    : [];\n\n  const handlers = (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {\n    const result: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void>\n    > = {};\n    for (const [name, actionFn] of actionMap) {\n      result[name] = async (params) => {\n        const setState = getSetState();\n        const state = getState();\n        if (setState) {\n          await actionFn(params, setState, state);\n        }\n      };\n    }\n    return result;\n  };\n\n  const executeAction = async (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state: StateModel = {},\n  ): Promise<void> => {\n    const entry = actionMap.find(([name]) => name === actionName);\n    if (entry) {\n      await entry[1](params, setState, state);\n    } else {\n      console.warn(`Unknown action: ${actionName}`);\n    }\n  };\n\n  return { registry, handlers, executeAction };\n}\n\n/** @internal */\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: React.ReactNode;\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => React.ReactNode;\n\n/** @internal */\ntype DefineRegistryActionFn = (\n  params: Record<string, unknown> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n// ============================================================================\n// NEW API\n// ============================================================================\n\n/**\n * Props for renderers created with createRenderer\n */\nexport interface CreateRendererProps {\n  /** The spec to render (AI-generated JSON) */\n  spec: Spec | null;\n  /**\n   * External store (controlled mode). When provided, `state` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** State context for dynamic values (uncontrolled mode) */\n  state?: Record<string, unknown>;\n  /** Action handler */\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Component map type - maps component names to React components\n */\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: ComponentType<\n    ComponentRenderProps<\n      TComponents[K][\"props\"] extends { _output: infer O }\n        ? O\n        : Record<string, unknown>\n    >\n  >;\n};\n\n/**\n * Create a renderer from a catalog\n *\n * @example\n * ```typescript\n * const DashboardRenderer = createRenderer(dashboardCatalog, {\n *   Card: ({ element, children }) => <div className=\"card\">{children}</div>,\n *   Metric: ({ element }) => <span>{element.props.value}</span>,\n * });\n *\n * // Usage\n * <DashboardRenderer spec={aiGeneratedSpec} state={state} />\n * ```\n */\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentType<CreateRendererProps> {\n  // Convert component map to registry\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  // Return the renderer component\n  return function CatalogRenderer({\n    spec,\n    store,\n    state,\n    onAction,\n    onStateChange,\n    functions,\n    loading,\n    fallback,\n  }: CreateRendererProps) {\n    // Wrap onAction with a Proxy so any action name routes to the callback\n    const actionHandlers = onAction\n      ? new Proxy(\n          {} as Record<\n            string,\n            (params: Record<string, unknown>) => void | Promise<void>\n          >,\n          {\n            get: (_target, prop: string) => {\n              return (params: Record<string, unknown>) =>\n                onAction(prop, params);\n            },\n            has: () => true,\n          },\n        )\n      : undefined;\n\n    return (\n      <StateProvider\n        store={store}\n        initialState={state}\n        onStateChange={onStateChange}\n      >\n        <VisibilityProvider>\n          <ValidationProvider>\n            <ActionProvider handlers={actionHandlers}>\n              <FunctionsContext.Provider value={functions ?? EMPTY_FUNCTIONS}>\n                <Renderer\n                  spec={spec}\n                  registry={registry}\n                  loading={loading}\n                  fallback={fallback}\n                />\n                <ConfirmationDialogManager />\n              </FunctionsContext.Provider>\n            </ActionProvider>\n          </ValidationProvider>\n        </VisibilityProvider>\n      </StateProvider>\n    );\n  };\n}\n"
  },
  {
    "path": "packages/react/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/react\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like\n    spec: s.object({\n      /** Root element key */\n      root: s.string(),\n      /** Flat map of elements by key */\n      elements: s.record(\n        s.object({\n          /** Component type from catalog */\n          type: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Child element keys (flat reference) */\n          children: s.array(s.string()),\n          /** Visibility condition */\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Component definitions */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n        slots: s.array(s.string()),\n        /** Description for AI generation hints */\n        description: s.string(),\n        /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n        example: s.any(),\n      }),\n      /** Action definitions (optional) */\n      actions: s.map({\n        /** Zod schema for action params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    builtInActions: [\n      {\n        name: \"setState\",\n        description:\n          \"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }\",\n      },\n      {\n        name: \"pushState\",\n        description:\n          'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {\"$state\":\"/path\"} refs and \"$id\" for auto IDs.',\n      },\n      {\n        name: \"removeState\",\n        description:\n          \"Remove an item from an array in state by index. Params: { statePath: string, index: number }\",\n      },\n      {\n        name: \"validateForm\",\n        description:\n          \"Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.\",\n      },\n    ],\n    defaultRules: [\n      // Element integrity\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n      \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n\n      // Field placement\n      'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"},\"children\":[...]}.',\n      'CRITICAL: The \"on\" field goes on the ELEMENT object, NOT inside \"props\". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',\n\n      // State and data\n      \"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).\",\n      'When building repeating content backed by a state array (e.g. posts, products, items), use the \"repeat\" field on a container element. Example: { \"type\": \"<ContainerComponent>\", \"props\": {}, \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" }, \"children\": [\"post-card\"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n      // Design quality\n      \"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.\",\n    ],\n  },\n);\n\n/**\n * Type for the React schema\n */\nexport type ReactSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type ReactSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n\n// Backward compatibility aliases\n/** @deprecated Use `schema` instead */\nexport const elementTreeSchema = schema;\n/** @deprecated Use `ReactSchema` instead */\nexport type ElementTreeSchema = ReactSchema;\n/** @deprecated Use `ReactSpec` instead */\nexport type ElementTreeSpec<T> = ReactSpec<T>;\n"
  },
  {
    "path": "packages/react/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/schema.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: { resolve: [\"@internal/react-state\"] },\n  sourcemap: true,\n  clean: true,\n  noExternal: [\"@internal/react-state\"],\n  external: [\"react\", \"react-dom\", \"@json-render/core\"],\n});\n"
  },
  {
    "path": "packages/react-email/CHANGELOG.md",
    "content": "# @json-render/react-email\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Minor Changes\n\n- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.\n\n  ### New:\n  - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.\n  - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.\n  - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.\n\n  ### Fixed:\n  - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n"
  },
  {
    "path": "packages/react-email/README.md",
    "content": "# @json-render/react-email\n\nReact Email renderer for `@json-render/core`. Generate HTML and plain-text emails from JSON specs using `@react-email/components` and `@react-email/render`.\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/react-email @react-email/components @react-email/render\n```\n\n## Quick Start\n\n### Render a spec to HTML\n\n```typescript\nimport { renderToHtml } from \"@json-render/react-email\";\nimport type { Spec } from \"@json-render/core\";\n\nconst spec: 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### With a custom catalog\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## Standard Components\n\n### Document structure\n\n| Component | Description |\n|-----------|-------------|\n| `Html` | Top-level email wrapper. Must be the root element. |\n| `Head` | Email head section. |\n| `Body` | Email body wrapper. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Constrains content width. |\n| `Section` | Groups related content. |\n| `Row` | Horizontal layout row. |\n| `Column` | Column within a Row. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (h1–h6). |\n| `Text` | Body text paragraph. |\n| `Link` | Hyperlink. |\n| `Button` | Call-to-action button. |\n| `Image` | Image from URL. |\n| `Hr` | Horizontal rule. |\n\n### Utility\n\n| Component | Description |\n|-----------|-------------|\n| `Preview` | Inbox preview text. |\n| `Markdown` | Markdown content as email-safe HTML. |\n\n## Server-Side APIs\n\n```typescript\nimport { renderToHtml, renderToPlainText } from \"@json-render/react-email\";\n\nconst html = await renderToHtml(spec);\nconst plainText = await renderToPlainText(spec);\n```\n\nBoth accept an optional second argument with:\n\n- `registry` — Custom component registry (merged with standard components)\n- `includeStandard` — Include built-in standard components (default: `true`)\n- `state` — Initial state for `$state` / `$cond` dynamic prop resolution\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React or `@react-email/components`:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-email/server\";\n```\n\n## Documentation\n\nFull API reference: [json-render.dev/docs/api/react-email](https://json-render.dev/docs/api/react-email).\n\nExample app: [examples/react-email](https://github.com/vercel-labs/json-render/tree/main/examples/react-email).\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/react-email/package.json",
    "content": "{\n  \"name\": \"@json-render/react-email\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"React Email renderer for @json-render/core. JSON becomes HTML emails.\",\n  \"keywords\": [\n    \"json\",\n    \"email\",\n    \"react-email\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"html\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/react-email\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.mjs\",\n      \"require\": \"./dist/server.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    },\n    \"./render\": {\n      \"types\": \"./dist/render.d.ts\",\n      \"import\": \"./dist/render.mjs\",\n      \"require\": \"./dist/render.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@react-email/components\": \"^1.0.8\",\n    \"@react-email/render\": \"^2.0.4\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.0.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.0.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-email/src/__fixtures__/examples.ts",
    "content": "import type { Spec } from \"@json-render/core\";\n\nexport interface Example {\n  name: string;\n  label: string;\n  description: string;\n  spec: Spec;\n}\n\n// Images sourced from the react-email demo; URLs are only used for spec\n// structure validation in tests and are not fetched at runtime.\nconst staticUrl = \"https://react-email-demo-bdj5iju9r-resend.vercel.app/static\";\n\nexport const examples: Example[] = [\n  {\n    name: \"vercel-invite\",\n    label: \"Vercel Invite\",\n    description: \"Team invitation email with avatars and CTA\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: { text: \"Join Alan on Vercel\" },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              fontFamily:\n                '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Ubuntu, sans-serif',\n              margin: \"0 auto\",\n              padding: \"0 8px\",\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              maxWidth: \"465px\",\n              margin: \"40px auto\",\n              border: \"1px solid #eaeaea\",\n              borderRadius: \"4px\",\n              padding: \"20px\",\n            },\n          },\n          children: [\n            \"logo-section\",\n            \"heading\",\n            \"greeting-text\",\n            \"invite-text\",\n            \"avatar-section\",\n            \"button-section\",\n            \"url-text\",\n            \"invite-link\",\n            \"hr\",\n            \"footer-text\",\n          ],\n        },\n        \"logo-section\": {\n          type: \"Section\",\n          props: { style: { marginTop: \"32px\" } },\n          children: [\"logo\"],\n        },\n        logo: {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-logo.png`,\n            width: 40,\n            height: 37,\n            alt: \"Vercel\",\n            style: { margin: \"0 auto\", display: \"block\" },\n          },\n          children: [],\n        },\n        heading: {\n          type: \"Heading\",\n          props: {\n            text: \"Join Enigma on Vercel\",\n            as: \"h1\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"24px\",\n              fontWeight: \"normal\",\n              textAlign: \"center\",\n              margin: \"30px 0\",\n              padding: \"0\",\n            },\n          },\n          children: [],\n        },\n        \"greeting-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Hello alanturing,\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"invite-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Alan (alan.turing@example.com) has invited you to the Enigma team on Vercel.\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"avatar-section\": {\n          type: \"Section\",\n          props: { style: {} },\n          children: [\"avatar-row\"],\n        },\n        \"avatar-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"user-avatar-col\", \"arrow-col\", \"team-avatar-col\"],\n        },\n        \"user-avatar-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"user-avatar\"],\n        },\n        \"user-avatar\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-user.png`,\n            width: 64,\n            height: 64,\n            alt: \"alanturing\",\n            style: { borderRadius: \"50%\", marginLeft: \"auto\" },\n          },\n          children: [],\n        },\n        \"arrow-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"arrow-img\"],\n        },\n        \"arrow-img\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-arrow.png`,\n            width: 12,\n            height: 9,\n            alt: \"invited to\",\n            style: { margin: \"0 auto\" },\n          },\n          children: [],\n        },\n        \"team-avatar-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"team-avatar\"],\n        },\n        \"team-avatar\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/vercel-team.png`,\n            width: 64,\n            height: 64,\n            alt: \"Enigma\",\n            style: { borderRadius: \"50%\", marginRight: \"auto\" },\n          },\n          children: [],\n        },\n        \"button-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              marginTop: \"32px\",\n              marginBottom: \"32px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [\"join-button\"],\n        },\n        \"join-button\": {\n          type: \"Button\",\n          props: {\n            text: \"Join the team\",\n            href: \"https://vercel.com/teams/invite/foo\",\n            style: {\n              backgroundColor: \"#000000\",\n              borderRadius: \"4px\",\n              color: \"#ffffff\",\n              fontSize: \"12px\",\n              fontWeight: \"600\",\n              textDecoration: \"none\",\n              textAlign: \"center\",\n              padding: \"12px 20px\",\n            },\n          },\n          children: [],\n        },\n        \"url-text\": {\n          type: \"Text\",\n          props: {\n            text: \"or copy and paste this URL into your browser:\",\n            style: {\n              color: \"#000000\",\n              fontSize: \"14px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n        \"invite-link\": {\n          type: \"Link\",\n          props: {\n            text: \"https://vercel.com/teams/invite/foo\",\n            href: \"https://vercel.com/teams/invite/foo\",\n            style: {\n              color: \"#2563eb\",\n              textDecoration: \"none\",\n              fontSize: \"14px\",\n            },\n          },\n          children: [],\n        },\n        hr: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#eaeaea\", margin: \"26px 0\" } },\n          children: [],\n        },\n        \"footer-text\": {\n          type: \"Text\",\n          props: {\n            text: \"This invitation was intended for alanturing. This invite was sent from 204.13.186.218 located in São Paulo, Brazil. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.\",\n            style: {\n              color: \"#666666\",\n              fontSize: \"12px\",\n              lineHeight: \"24px\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n  {\n    name: \"stripe-welcome\",\n    label: \"Stripe Welcome\",\n    description: \"Onboarding email with dashboard CTA\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: {\n            text: \"You're now ready to make live transactions with Stripe!\",\n          },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#f6f9fc\",\n              fontFamily:\n                '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Ubuntu, sans-serif',\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              margin: \"0 auto\",\n              padding: \"20px 0 48px\",\n              marginBottom: \"64px\",\n            },\n          },\n          children: [\"content-section\"],\n        },\n        \"content-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"0 48px\" } },\n          children: [\n            \"logo\",\n            \"hr1\",\n            \"text-intro\",\n            \"text-dashboard\",\n            \"cta-button\",\n            \"hr2\",\n            \"text-docs\",\n            \"docs-link\",\n            \"text-api-keys\",\n            \"text-checklist\",\n            \"text-support\",\n            \"text-signoff\",\n            \"hr3\",\n            \"footer-text\",\n          ],\n        },\n        logo: {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/stripe-logo.png`,\n            width: 49,\n            height: 21,\n            alt: \"Stripe\",\n            style: null,\n          },\n          children: [],\n        },\n        hr1: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"text-intro\": {\n          type: \"Text\",\n          props: {\n            text: \"Thanks for submitting your account information. You're now ready to make live transactions with Stripe!\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-dashboard\": {\n          type: \"Text\",\n          props: {\n            text: \"You can view your payments and a variety of other information about your account right from your dashboard.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"cta-button\": {\n          type: \"Button\",\n          props: {\n            text: \"View your Stripe Dashboard\",\n            href: \"https://dashboard.stripe.com/login\",\n            style: {\n              backgroundColor: \"#656ee8\",\n              borderRadius: \"3px\",\n              color: \"#ffffff\",\n              fontSize: \"16px\",\n              fontWeight: \"bold\",\n              textDecoration: \"none\",\n              textAlign: \"center\",\n              display: \"block\",\n              padding: \"10px\",\n            },\n          },\n          children: [],\n        },\n        hr2: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"text-docs\": {\n          type: \"Text\",\n          props: {\n            text: \"If you haven't finished your integration, you might find our docs handy.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"docs-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Stripe Documentation — Getting Started\",\n            href: \"https://docs.stripe.com/dashboard/basics\",\n            style: {\n              color: \"#556cd6\",\n              fontSize: \"16px\",\n            },\n          },\n          children: [],\n        },\n        \"text-api-keys\": {\n          type: \"Text\",\n          props: {\n            text: \"Once you're ready to start accepting payments, you'll just need to use your live API keys instead of your test API keys. Your account can simultaneously be used for both test and live requests, so you can continue testing while accepting live payments. Check out our tutorial about account basics.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-checklist\": {\n          type: \"Text\",\n          props: {\n            text: \"Finally, we've put together a quick checklist to ensure your website conforms to card network standards.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-support\": {\n          type: \"Text\",\n          props: {\n            text: \"We'll be here to help you with any step along the way. You can find answers to most questions and get in touch with us on our support site.\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        \"text-signoff\": {\n          type: \"Text\",\n          props: {\n            text: \"— The Stripe team\",\n            style: {\n              color: \"#525f7f\",\n              fontSize: \"16px\",\n              lineHeight: \"24px\",\n              textAlign: \"left\",\n            },\n          },\n          children: [],\n        },\n        hr3: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" } },\n          children: [],\n        },\n        \"footer-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080\",\n            style: {\n              color: \"#8898aa\",\n              fontSize: \"12px\",\n              lineHeight: \"16px\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n  {\n    name: \"nike-receipt\",\n    label: \"Nike Receipt\",\n    description: \"Order shipment notification with product details\",\n    spec: {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: {\n          type: \"Head\",\n          props: {},\n          children: [],\n        },\n        preview: {\n          type: \"Preview\",\n          props: {\n            text: \"Get your order summary, estimated delivery date and more\",\n          },\n          children: [],\n        },\n        body: {\n          type: \"Body\",\n          props: {\n            style: {\n              backgroundColor: \"#ffffff\",\n              fontFamily: '\"Helvetica Neue\", Helvetica, Arial, sans-serif',\n            },\n          },\n          children: [\"container\"],\n        },\n        container: {\n          type: \"Container\",\n          props: {\n            style: {\n              margin: \"10px auto\",\n              width: \"600px\",\n              maxWidth: \"100%\",\n              border: \"1px solid #E5E5E5\",\n            },\n          },\n          children: [\n            \"tracking-section\",\n            \"hr1\",\n            \"hero-section\",\n            \"hr2\",\n            \"shipping-section\",\n            \"hr3\",\n            \"product-section\",\n            \"hr4\",\n            \"order-info-section\",\n            \"hr5\",\n            \"footer-section\",\n          ],\n        },\n\n        // ── Tracking Section ──\n        \"tracking-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              padding: \"22px 40px\",\n              backgroundColor: \"#F7F7F7\",\n            },\n          },\n          children: [\"tracking-row\"],\n        },\n        \"tracking-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"tracking-info-col\", \"tracking-btn-col\"],\n        },\n        \"tracking-info-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"tracking-label\", \"tracking-number\"],\n        },\n        \"tracking-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Tracking Number\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"tracking-number\": {\n          type: \"Text\",\n          props: {\n            text: \"1ZV218970300071628\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"tracking-btn-col\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"right\" } },\n          children: [\"tracking-link\"],\n        },\n        \"tracking-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Track Package\",\n            href: \"https://www.nike.com/orders\",\n            style: {\n              border: \"1px solid #929292\",\n              fontSize: \"16px\",\n              textDecoration: \"none\",\n              padding: \"10px 0\",\n              width: \"220px\",\n              display: \"block\",\n              textAlign: \"center\",\n              fontWeight: \"500\",\n              color: \"#000000\",\n            },\n          },\n          children: [],\n        },\n        hr1: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Hero Section ──\n        \"hero-section\": {\n          type: \"Section\",\n          props: {\n            style: {\n              padding: \"40px 74px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [\n            \"nike-logo\",\n            \"hero-heading\",\n            \"hero-text\",\n            \"hero-text-payment\",\n          ],\n        },\n        \"nike-logo\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/nike-logo.png`,\n            width: 66,\n            height: 22,\n            alt: \"Nike\",\n            style: { margin: \"0 auto\", display: \"block\" },\n          },\n          children: [],\n        },\n        \"hero-heading\": {\n          type: \"Heading\",\n          props: {\n            text: \"It's On Its Way.\",\n            as: \"h1\",\n            style: {\n              fontSize: \"32px\",\n              lineHeight: \"1.3\",\n              fontWeight: \"bold\",\n              textAlign: \"center\",\n              letterSpacing: \"-1px\",\n            },\n          },\n          children: [],\n        },\n        \"hero-text\": {\n          type: \"Text\",\n          props: {\n            text: \"Your order is on its way. Use the link above to track its progress.\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        \"hero-text-payment\": {\n          type: \"Text\",\n          props: {\n            text: \"We've also charged your payment method for the cost of your order and will be removing any authorization holds. For payment details, please visit your Orders page on Nike.com or in the Nike app.\",\n            style: {\n              margin: \"24px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr2: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Shipping Section ──\n        \"shipping-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 40px\" } },\n          children: [\"shipping-label\", \"shipping-address\"],\n        },\n        \"shipping-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Shipping to: Alan Turing\",\n            style: {\n              margin: \"0\",\n              fontSize: \"15px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"shipping-address\": {\n          type: \"Text\",\n          props: {\n            text: \"2125 Chestnut St, San Francisco, CA 94123\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr3: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Product Section ──\n        \"product-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"40px\" } },\n          children: [\"product-row\"],\n        },\n        \"product-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"product-img-col\", \"product-details-col\"],\n        },\n        \"product-img-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"product-img\"],\n        },\n        \"product-img\": {\n          type: \"Image\",\n          props: {\n            src: `${staticUrl}/nike-product.png`,\n            alt: \"Brazil 2022/23 Stadium Away Women's Nike Dri-FIT Soccer Jersey\",\n            width: 260,\n            height: null,\n            style: { float: \"left\" },\n          },\n          children: [],\n        },\n        \"product-details-col\": {\n          type: \"Column\",\n          props: {\n            style: { verticalAlign: \"top\", paddingLeft: \"12px\" },\n          },\n          children: [\"product-name\", \"product-size\"],\n        },\n        \"product-name\": {\n          type: \"Text\",\n          props: {\n            text: \"Brazil 2022/23 Stadium Away Women's Nike Dri-FIT Soccer Jersey\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        \"product-size\": {\n          type: \"Text\",\n          props: {\n            text: \"Size L (12–14)\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              color: \"#747474\",\n              fontWeight: \"500\",\n            },\n          },\n          children: [],\n        },\n        hr4: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Order Info Section ──\n        \"order-info-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 40px\" } },\n          children: [\"order-meta-row\", \"order-status-row\"],\n        },\n        \"order-meta-row\": {\n          type: \"Row\",\n          props: { style: { marginBottom: \"40px\" } },\n          children: [\"order-number-col\", \"order-date-col\"],\n        },\n        \"order-number-col\": {\n          type: \"Column\",\n          props: { style: { width: \"170px\" } },\n          children: [\"order-number-label\", \"order-number-value\"],\n        },\n        \"order-number-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Order Number\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"order-number-value\": {\n          type: \"Text\",\n          props: {\n            text: \"C0106373851\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"order-date-col\": {\n          type: \"Column\",\n          props: { style: {} },\n          children: [\"order-date-label\", \"order-date-value\"],\n        },\n        \"order-date-label\": {\n          type: \"Text\",\n          props: {\n            text: \"Order Date\",\n            style: {\n              margin: \"0\",\n              fontSize: \"14px\",\n              lineHeight: \"2\",\n              fontWeight: \"bold\",\n            },\n          },\n          children: [],\n        },\n        \"order-date-value\": {\n          type: \"Text\",\n          props: {\n            text: \"Sep 22, 2022\",\n            style: {\n              margin: \"12px 0 0\",\n              fontSize: \"14px\",\n              lineHeight: \"1.4\",\n              fontWeight: \"500\",\n              color: \"#6F6F6F\",\n            },\n          },\n          children: [],\n        },\n        \"order-status-row\": {\n          type: \"Row\",\n          props: { style: {} },\n          children: [\"order-status-col\"],\n        },\n        \"order-status-col\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"order-status-link\"],\n        },\n        \"order-status-link\": {\n          type: \"Link\",\n          props: {\n            text: \"Order Status\",\n            href: \"https://www.nike.com/orders\",\n            style: {\n              border: \"1px solid #929292\",\n              fontSize: \"16px\",\n              textDecoration: \"none\",\n              padding: \"10px 0\",\n              width: \"220px\",\n              display: \"block\",\n              textAlign: \"center\",\n              fontWeight: \"500\",\n              color: \"#000000\",\n              margin: \"0 auto\",\n            },\n          },\n          children: [],\n        },\n        hr5: {\n          type: \"Hr\",\n          props: { style: { borderColor: \"#E5E5E5\", margin: \"0\" } },\n          children: [],\n        },\n\n        // ── Footer Section ──\n        \"footer-section\": {\n          type: \"Section\",\n          props: { style: { padding: \"22px 0\", textAlign: \"center\" } },\n          children: [\n            \"footer-brand\",\n            \"footer-nav-row\",\n            \"footer-contact\",\n            \"footer-copyright\",\n            \"footer-address\",\n          ],\n        },\n        \"footer-brand\": {\n          type: \"Text\",\n          props: {\n            text: \"Nike.com\",\n            style: {\n              fontSize: \"32px\",\n              lineHeight: \"1.3\",\n              fontWeight: \"bold\",\n              textAlign: \"center\",\n              letterSpacing: \"-1px\",\n            },\n          },\n          children: [],\n        },\n        \"footer-nav-row\": {\n          type: \"Row\",\n          props: { style: { width: \"370px\", margin: \"0 auto\" } },\n          children: [\n            \"footer-nav-men\",\n            \"footer-nav-women\",\n            \"footer-nav-kids\",\n            \"footer-nav-customize\",\n          ],\n        },\n        \"footer-nav-men\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-men\"],\n        },\n        \"link-men\": {\n          type: \"Link\",\n          props: {\n            text: \"Men\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-women\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-women\"],\n        },\n        \"link-women\": {\n          type: \"Link\",\n          props: {\n            text: \"Women\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-kids\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-kids\"],\n        },\n        \"link-kids\": {\n          type: \"Link\",\n          props: {\n            text: \"Kids\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-nav-customize\": {\n          type: \"Column\",\n          props: { style: { textAlign: \"center\" } },\n          children: [\"link-customize\"],\n        },\n        \"link-customize\": {\n          type: \"Link\",\n          props: {\n            text: \"Customize\",\n            href: \"https://www.nike.com/\",\n            style: { fontWeight: \"500\", color: \"#000000\" },\n          },\n          children: [],\n        },\n        \"footer-contact\": {\n          type: \"Text\",\n          props: {\n            text: \"Please contact us if you have any questions. (If you reply to this email, we won't be able to see it.)\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n              padding: \"30px 0\",\n            },\n          },\n          children: [],\n        },\n        \"footer-copyright\": {\n          type: \"Text\",\n          props: {\n            text: \"© 2022 Nike, Inc. All Rights Reserved.\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [],\n        },\n        \"footer-address\": {\n          type: \"Text\",\n          props: {\n            text: \"NIKE, INC. One Bowerman Drive, Beaverton, Oregon 97005, USA.\",\n            style: {\n              margin: \"0\",\n              color: \"#AFAFAF\",\n              fontSize: \"13px\",\n              textAlign: \"center\",\n            },\n          },\n          children: [],\n        },\n      },\n    },\n  },\n];\n"
  },
  {
    "path": "packages/react-email/src/catalog-types.ts",
    "content": "import type { ReactNode } from \"react\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferComponentProps,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> {\n  props: InferComponentProps<C, K>;\n  children?: ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => ReactNode;\n\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n"
  },
  {
    "path": "packages/react-email/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\nconst styleSchema = z.record(z.string(), z.any()).nullable();\n\n/**\n * Standard component definitions for React Email catalogs.\n *\n * These define the available email components with their Zod prop schemas.\n * All components render using @react-email/components primitives.\n */\nexport const standardComponentDefinitions = {\n  // ==========================================================================\n  // Document Structure\n  // ==========================================================================\n\n  Html: {\n    props: z.object({\n      lang: z.string().nullable(),\n      dir: z.enum([\"ltr\", \"rtl\"]).nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Top-level HTML email wrapper. Must be the root element. Children should include Head and Body.\",\n    example: { lang: \"en\", dir: \"ltr\" },\n  },\n\n  Head: {\n    props: z.object({}),\n    slots: [\"default\"],\n    description:\n      \"Email head section. Place inside Html. Can contain metadata but typically left empty.\",\n    example: {},\n  },\n\n  Body: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Email body wrapper. Place inside Html after Head. Contains all visible email content.\",\n    example: { style: { backgroundColor: \"#f6f9fc\" } },\n  },\n\n  Container: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Constrains content width for email clients. Place inside Body. Typically max-width 600px.\",\n    example: {\n      style: {\n        maxWidth: \"600px\",\n        margin: \"0 auto\",\n        padding: \"20px 0 48px\",\n      },\n    },\n  },\n\n  Section: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Groups related content. Renders as a table-based section for email compatibility.\",\n    example: { style: { padding: \"24px\", backgroundColor: \"#ffffff\" } },\n  },\n\n  Row: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Horizontal layout row. Use inside Section for multi-column layouts.\",\n    example: { style: {} },\n  },\n\n  Column: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Column within a Row. Set width via style for proportional layouts.\",\n    example: { style: { width: \"50%\" } },\n  },\n\n  // ==========================================================================\n  // Content Components\n  // ==========================================================================\n\n  Heading: {\n    props: z.object({\n      text: z.string(),\n      as: z.enum([\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"]).nullable(),\n      style: styleSchema,\n    }),\n    slots: [],\n    description:\n      \"Heading text at various levels. h1 is largest, h6 is smallest.\",\n    example: { text: \"Welcome!\", as: \"h1\" },\n  },\n\n  Text: {\n    props: z.object({\n      text: z.string(),\n      style: styleSchema,\n    }),\n    slots: [],\n    description:\n      \"Body text paragraph. Use style for font size, color, weight, and alignment.\",\n    example: { text: \"Thank you for signing up.\" },\n  },\n\n  Link: {\n    props: z.object({\n      text: z.string(),\n      href: z.string(),\n      style: styleSchema,\n    }),\n    slots: [],\n    description: \"Hyperlink with visible text and a URL.\",\n    example: {\n      text: \"Visit our website\",\n      href: \"https://example.com\",\n      style: { color: \"#2563eb\" },\n    },\n  },\n\n  Button: {\n    props: z.object({\n      text: z.string(),\n      href: z.string(),\n      style: styleSchema,\n    }),\n    slots: [],\n    description:\n      \"Call-to-action button rendered as a link styled as a button. Provide text and href.\",\n    example: {\n      text: \"Get Started\",\n      href: \"https://example.com\",\n      style: {\n        backgroundColor: \"#5F51E8\",\n        borderRadius: \"3px\",\n        color: \"#fff\",\n        padding: \"12px 20px\",\n      },\n    },\n  },\n\n  Image: {\n    props: z.object({\n      src: z.string(),\n      alt: z.string().nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      style: styleSchema,\n    }),\n    slots: [],\n    description:\n      \"Image from a URL. src must be a fully qualified URL. Specify width and height for consistent rendering.\",\n    example: {\n      src: \"https://picsum.photos/400/200?random=1\",\n      alt: \"Hero image\",\n      width: 400,\n      height: 200,\n    },\n  },\n\n  Hr: {\n    props: z.object({\n      style: styleSchema,\n    }),\n    slots: [],\n    description: \"Horizontal rule separator between content sections.\",\n    example: {\n      style: { borderColor: \"#e6ebf1\", margin: \"20px 0\" },\n    },\n  },\n\n  // ==========================================================================\n  // Utility Components\n  // ==========================================================================\n\n  Preview: {\n    props: z.object({\n      text: z.string(),\n    }),\n    slots: [],\n    description:\n      \"Preview text shown in email client inboxes before the email is opened. Place inside Html.\",\n    example: { text: \"You have a new message from Acme Corp\" },\n  },\n\n  Markdown: {\n    props: z.object({\n      content: z.string(),\n      markdownContainerStyles: styleSchema,\n      markdownCustomStyles: z.record(z.string(), z.any()).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Renders markdown content as email-safe HTML. Supports headings, paragraphs, lists, links, bold, italic, and code.\",\n    example: {\n      content: \"# Hello\\n\\nThis is **bold** and *italic* text.\",\n    },\n  },\n};\n\nexport type StandardComponentDefinitions = typeof standardComponentDefinitions;\n\nexport type StandardComponentProps<\n  K extends keyof StandardComponentDefinitions,\n> = StandardComponentDefinitions[K][\"props\"] extends { _output: infer O }\n  ? O\n  : z.output<StandardComponentDefinitions[K][\"props\"]>;\n"
  },
  {
    "path": "packages/react-email/src/components/index.ts",
    "content": "export { standardComponents } from \"./standard\";\n"
  },
  {
    "path": "packages/react-email/src/components/standard.tsx",
    "content": "import React from \"react\";\nimport {\n  Html as EmailHtml,\n  Head as EmailHead,\n  Body as EmailBody,\n  Container as EmailContainer,\n  Section as EmailSection,\n  Row as EmailRow,\n  Column as EmailColumn,\n  Heading as EmailHeading,\n  Text as EmailText,\n  Link as EmailLink,\n  Button as EmailButton,\n  Img as EmailImg,\n  Hr as EmailHr,\n  Preview as EmailPreview,\n  Markdown as EmailMarkdown,\n} from \"@react-email/components\";\nimport type { ComponentRenderProps } from \"../renderer\";\nimport type { ComponentRegistry } from \"../renderer\";\nimport type { StandardComponentProps } from \"../catalog\";\n\n// =============================================================================\n// Document Structure\n// =============================================================================\n\nfunction HtmlComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Html\">>) {\n  const p = element.props;\n\n  return (\n    <EmailHtml lang={p.lang ?? undefined} dir={p.dir ?? undefined}>\n      {children}\n    </EmailHtml>\n  );\n}\n\nfunction HeadComponent({\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Head\">>) {\n  return <EmailHead>{children}</EmailHead>;\n}\n\nfunction BodyComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Body\">>) {\n  const p = element.props;\n\n  return <EmailBody style={p.style ?? undefined}>{children}</EmailBody>;\n}\n\nfunction ContainerComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Container\">>) {\n  const p = element.props;\n\n  return (\n    <EmailContainer style={p.style ?? undefined}>{children}</EmailContainer>\n  );\n}\n\nfunction SectionComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Section\">>) {\n  const p = element.props;\n\n  return <EmailSection style={p.style ?? undefined}>{children}</EmailSection>;\n}\n\nfunction RowComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Row\">>) {\n  const p = element.props;\n\n  return <EmailRow style={p.style ?? undefined}>{children}</EmailRow>;\n}\n\nfunction ColumnComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Column\">>) {\n  const p = element.props;\n\n  return <EmailColumn style={p.style ?? undefined}>{children}</EmailColumn>;\n}\n\n// =============================================================================\n// Content Components\n// =============================================================================\n\nfunction HeadingComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Heading\">>) {\n  const p = element.props;\n\n  return (\n    <EmailHeading as={p.as ?? \"h2\"} style={p.style ?? undefined}>\n      {p.text}\n    </EmailHeading>\n  );\n}\n\nfunction TextComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Text\">>) {\n  const p = element.props;\n\n  return <EmailText style={p.style ?? undefined}>{p.text}</EmailText>;\n}\n\nfunction LinkComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Link\">>) {\n  const p = element.props;\n\n  return (\n    <EmailLink href={p.href} style={p.style ?? undefined}>\n      {p.text}\n    </EmailLink>\n  );\n}\n\nfunction ButtonComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Button\">>) {\n  const p = element.props;\n\n  return (\n    <EmailButton href={p.href} style={p.style ?? undefined}>\n      {p.text}\n    </EmailButton>\n  );\n}\n\nfunction ImageComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Image\">>) {\n  const p = element.props;\n\n  return (\n    <EmailImg\n      src={p.src}\n      alt={p.alt ?? undefined}\n      width={p.width ?? undefined}\n      height={p.height ?? undefined}\n      style={p.style ?? undefined}\n    />\n  );\n}\n\nfunction HrComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Hr\">>) {\n  const p = element.props;\n\n  return <EmailHr style={p.style ?? undefined} />;\n}\n\n// =============================================================================\n// Utility Components\n// =============================================================================\n\nfunction PreviewComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Preview\">>) {\n  const p = element.props;\n\n  return <EmailPreview>{p.text}</EmailPreview>;\n}\n\nfunction MarkdownComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Markdown\">>) {\n  const p = element.props;\n\n  return (\n    <EmailMarkdown\n      markdownContainerStyles={p.markdownContainerStyles ?? undefined}\n      markdownCustomStyles={p.markdownCustomStyles ?? undefined}\n    >\n      {p.content}\n    </EmailMarkdown>\n  );\n}\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport const standardComponents: ComponentRegistry = {\n  Html: HtmlComponent,\n  Head: HeadComponent,\n  Body: BodyComponent,\n  Container: ContainerComponent,\n  Section: SectionComponent,\n  Row: RowComponent,\n  Column: ColumnComponent,\n  Heading: HeadingComponent,\n  Text: TextComponent,\n  Link: LinkComponent,\n  Button: ButtonComponent,\n  Image: ImageComponent,\n  Hr: HrComponent,\n  Preview: PreviewComponent,\n  Markdown: MarkdownComponent,\n};\n"
  },
  {
    "path": "packages/react-email/src/contexts/actions.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nfunction generateUniqueId(): string {\n  return crypto.randomUUID();\n}\n\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\nexport interface PendingConfirmation {\n  action: ResolvedAction;\n  handler: ActionHandler;\n  resolve: () => void;\n  reject: () => void;\n}\n\nexport interface ActionContextValue {\n  handlers: Record<string, ActionHandler>;\n  loadingActions: Set<string>;\n  pendingConfirmation: PendingConfirmation | null;\n  execute: (binding: ActionBinding) => Promise<void>;\n  confirm: () => void;\n  cancel: () => void;\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ActionContext = createContext<ActionContextValue | null>(null);\n\nexport interface ActionProviderProps {\n  handlers?: Record<string, ActionHandler>;\n  navigate?: (path: string) => void;\n  children: ReactNode;\n}\n\nexport function ActionProvider({\n  handlers: initialHandlers = {},\n  navigate,\n  children,\n}: ActionProviderProps) {\n  const { state, get, set } = useStateStore();\n  const [handlers, setHandlers] =\n    useState<Record<string, ActionHandler>>(initialHandlers);\n  const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());\n  const [pendingConfirmation, setPendingConfirmation] =\n    useState<PendingConfirmation | null>(null);\n\n  const registerHandler = useCallback(\n    (name: string, handler: ActionHandler) => {\n      setHandlers((prev) => ({ ...prev, [name]: handler }));\n    },\n    [],\n  );\n\n  const execute = useCallback(\n    async (binding: ActionBinding) => {\n      const resolved = resolveAction(binding, state);\n\n      if (resolved.action === \"setState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const value = resolved.params.value;\n        if (statePath) {\n          set(statePath, value);\n        }\n        return;\n      }\n\n      if (resolved.action === \"pushState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const rawValue = resolved.params.value;\n        if (statePath) {\n          const resolvedValue = deepResolveValue(rawValue, get);\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(statePath, [...arr, resolvedValue]);\n          const clearStatePath = resolved.params.clearStatePath as\n            | string\n            | undefined;\n          if (clearStatePath) {\n            set(clearStatePath, \"\");\n          }\n        }\n        return;\n      }\n\n      if (resolved.action === \"removeState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const index = resolved.params.index as number;\n        if (statePath !== undefined && index !== undefined) {\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(\n            statePath,\n            arr.filter((_, i) => i !== index),\n          );\n        }\n        return;\n      }\n\n      const handler = handlers[resolved.action];\n\n      if (!handler) {\n        console.warn(`No handler registered for action: ${resolved.action}`);\n        return;\n      }\n\n      if (resolved.confirm) {\n        return new Promise<void>((resolve, reject) => {\n          setPendingConfirmation({\n            action: resolved,\n            handler,\n            resolve: () => {\n              setPendingConfirmation(null);\n              resolve();\n            },\n            reject: () => {\n              setPendingConfirmation(null);\n              reject(new Error(\"Action cancelled\"));\n            },\n          });\n        }).then(async () => {\n          setLoadingActions((prev) => new Set(prev).add(resolved.action));\n          try {\n            await executeAction({\n              action: resolved,\n              handler,\n              setState: set,\n              navigate,\n              executeAction: async (name) => {\n                const subBinding: ActionBinding = { action: name };\n                await execute(subBinding);\n              },\n            });\n          } finally {\n            setLoadingActions((prev) => {\n              const next = new Set(prev);\n              next.delete(resolved.action);\n              return next;\n            });\n          }\n        });\n      }\n\n      setLoadingActions((prev) => new Set(prev).add(resolved.action));\n      try {\n        await executeAction({\n          action: resolved,\n          handler,\n          setState: set,\n          navigate,\n          executeAction: async (name) => {\n            const subBinding: ActionBinding = { action: name };\n            await execute(subBinding);\n          },\n        });\n      } finally {\n        setLoadingActions((prev) => {\n          const next = new Set(prev);\n          next.delete(resolved.action);\n          return next;\n        });\n      }\n    },\n    [state, handlers, get, set, navigate],\n  );\n\n  const confirm = useCallback(() => {\n    pendingConfirmation?.resolve();\n  }, [pendingConfirmation]);\n\n  const cancel = useCallback(() => {\n    pendingConfirmation?.reject();\n  }, [pendingConfirmation]);\n\n  const value = useMemo<ActionContextValue>(\n    () => ({\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    }),\n    [\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    ],\n  );\n\n  return (\n    <ActionContext.Provider value={value}>{children}</ActionContext.Provider>\n  );\n}\n\nexport function useActions(): ActionContextValue {\n  const ctx = useContext(ActionContext);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: boolean;\n} {\n  const { execute, loadingActions } = useActions();\n  const isLoading = loadingActions.has(binding.action);\n\n  const executeAction = useCallback(() => execute(binding), [execute, binding]);\n\n  return { execute: executeAction, isLoading };\n}\n\nexport interface ConfirmDialogProps {\n  confirm: ActionConfirm;\n  onConfirm: () => void;\n  onCancel: () => void;\n}\n\n/**\n * No-op confirm dialog for email context. Emails are non-interactive,\n * so confirmations are not rendered.\n */\nexport function ConfirmDialog(_props: ConfirmDialogProps) {\n  return null;\n}\n"
  },
  {
    "path": "packages/react-email/src/contexts/repeat-scope.tsx",
    "content": "import React, { createContext, useContext, type ReactNode } from \"react\";\n\nexport interface RepeatScopeValue {\n  item: unknown;\n  index: number;\n  basePath: string;\n}\n\nconst RepeatScopeContext = createContext<RepeatScopeValue | null>(null);\n\nexport function RepeatScopeProvider({\n  item,\n  index,\n  basePath,\n  children,\n}: RepeatScopeValue & { children: ReactNode }) {\n  return (\n    <RepeatScopeContext.Provider value={{ item, index, basePath }}>\n      {children}\n    </RepeatScopeContext.Provider>\n  );\n}\n\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return useContext(RepeatScopeContext);\n}\n"
  },
  {
    "path": "packages/react-email/src/contexts/state.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  useEffect,\n  useRef,\n  type ReactNode,\n} from \"react\";\nimport { getByPath, setByPath, type StateModel } from \"@json-render/core\";\n\nexport interface StateContextValue {\n  state: StateModel;\n  get: (path: string) => unknown;\n  set: (path: string, value: unknown) => void;\n  update: (updates: Record<string, unknown>) => void;\n}\n\nconst StateContext = createContext<StateContextValue | null>(null);\n\nexport interface StateProviderProps {\n  initialState?: StateModel;\n  onStateChange?: (path: string, value: unknown) => void;\n  children: ReactNode;\n}\n\nexport function StateProvider({\n  initialState = {},\n  onStateChange,\n  children,\n}: StateProviderProps) {\n  const [state, setStateInternal] = useState<StateModel>(initialState);\n\n  const stateRef = useRef(state);\n  stateRef.current = state;\n\n  const initialStateJsonRef = useRef<string>(JSON.stringify(initialState));\n\n  useEffect(() => {\n    const newJson = JSON.stringify(initialState);\n    if (newJson !== initialStateJsonRef.current) {\n      initialStateJsonRef.current = newJson;\n      if (initialState && Object.keys(initialState).length > 0) {\n        setStateInternal((prev) => ({ ...prev, ...initialState }));\n      }\n    }\n  }, [initialState]);\n\n  const get = useCallback(\n    (path: string) => getByPath(stateRef.current, path),\n    [],\n  );\n\n  const set = useCallback(\n    (path: string, value: unknown) => {\n      setStateInternal((prev) => {\n        const next = { ...prev };\n        setByPath(next, path, value);\n        return next;\n      });\n      onStateChange?.(path, value);\n    },\n    [onStateChange],\n  );\n\n  const update = useCallback(\n    (updates: Record<string, unknown>) => {\n      const entries = Object.entries(updates);\n      setStateInternal((prev) => {\n        const next = { ...prev };\n        for (const [path, value] of entries) {\n          setByPath(next, path, value);\n        }\n        return next;\n      });\n      for (const [path, value] of entries) {\n        onStateChange?.(path, value);\n      }\n    },\n    [onStateChange],\n  );\n\n  const value = useMemo<StateContextValue>(\n    () => ({ state, get, set, update }),\n    [state, get, set, update],\n  );\n\n  return (\n    <StateContext.Provider value={value}>{children}</StateContext.Provider>\n  );\n}\n\nexport function useStateStore(): StateContextValue {\n  const ctx = useContext(StateContext);\n  if (!ctx) {\n    throw new Error(\"useStateStore must be used within a StateProvider\");\n  }\n  return ctx;\n}\n\nexport function useStateValue<T>(path: string): T | undefined {\n  const { state } = useStateStore();\n  return getByPath(state, path) as T | undefined;\n}\n\nexport function useStateBinding<T>(\n  path: string,\n): [T | undefined, (value: T) => void] {\n  const { state, set } = useStateStore();\n  const value = getByPath(state, path) as T | undefined;\n  const setValue = useCallback(\n    (newValue: T) => set(path, newValue),\n    [path, set],\n  );\n  return [value, setValue];\n}\n"
  },
  {
    "path": "packages/react-email/src/contexts/validation.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface FieldValidationState {\n  touched: boolean;\n  validated: boolean;\n  result: ValidationResult | null;\n}\n\nexport interface ValidationContextValue {\n  customFunctions: Record<string, ValidationFunction>;\n  fieldStates: Record<string, FieldValidationState>;\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  touch: (path: string) => void;\n  clear: (path: string) => void;\n  validateAll: () => boolean;\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst ValidationContext = createContext<ValidationContextValue | null>(null);\n\nexport interface ValidationProviderProps {\n  customFunctions?: Record<string, ValidationFunction>;\n  children: ReactNode;\n}\n\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n  if (a.validateOn !== b.validateOn) return false;\n\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n\n  return true;\n}\n\nexport function ValidationProvider({\n  customFunctions = {},\n  children,\n}: ValidationProviderProps) {\n  const { state } = useStateStore();\n  const [fieldStates, setFieldStates] = useState<\n    Record<string, FieldValidationState>\n  >({});\n  const [fieldConfigs, setFieldConfigs] = useState<\n    Record<string, ValidationConfig>\n  >({});\n\n  const registerField = useCallback(\n    (path: string, config: ValidationConfig) => {\n      setFieldConfigs((prev) => {\n        const existing = prev[path];\n        if (existing && validationConfigEqual(existing, config)) {\n          return prev;\n        }\n        return { ...prev, [path]: config };\n      });\n    },\n    [],\n  );\n\n  const validate = useCallback(\n    (path: string, config: ValidationConfig): ValidationResult => {\n      const segments = path.split(\"/\").filter(Boolean);\n      let value: unknown = state;\n      for (const seg of segments) {\n        if (value != null && typeof value === \"object\") {\n          value = (value as Record<string, unknown>)[seg];\n        } else {\n          value = undefined;\n          break;\n        }\n      }\n      const result = runValidation(config, {\n        value,\n        stateModel: state,\n        customFunctions,\n      });\n\n      setFieldStates((prev) => ({\n        ...prev,\n        [path]: {\n          touched: prev[path]?.touched ?? true,\n          validated: true,\n          result,\n        },\n      }));\n\n      return result;\n    },\n    [state, customFunctions],\n  );\n\n  const touch = useCallback((path: string) => {\n    setFieldStates((prev) => ({\n      ...prev,\n      [path]: {\n        ...prev[path],\n        touched: true,\n        validated: prev[path]?.validated ?? false,\n        result: prev[path]?.result ?? null,\n      },\n    }));\n  }, []);\n\n  const clear = useCallback((path: string) => {\n    setFieldStates((prev) => {\n      const { [path]: _, ...rest } = prev;\n      return rest;\n    });\n  }, []);\n\n  const validateAll = useCallback(() => {\n    let allValid = true;\n    for (const [path, config] of Object.entries(fieldConfigs)) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n    return allValid;\n  }, [fieldConfigs, validate]);\n\n  const value = useMemo<ValidationContextValue>(\n    () => ({\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    }),\n    [\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    ],\n  );\n\n  return (\n    <ValidationContext.Provider value={value}>\n      {children}\n    </ValidationContext.Provider>\n  );\n}\n\nexport function useValidation(): ValidationContextValue {\n  const ctx = useContext(ValidationContext);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: FieldValidationState;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: string[];\n  isValid: boolean;\n} {\n  const {\n    fieldStates,\n    validate: validateField,\n    touch: touchField,\n    clear: clearField,\n    registerField,\n  } = useValidation();\n\n  React.useEffect(() => {\n    if (config) {\n      registerField(path, config);\n    }\n  }, [path, config, registerField]);\n\n  const state = fieldStates[path] ?? {\n    touched: false,\n    validated: false,\n    result: null,\n  };\n\n  const validate = useCallback(\n    () => validateField(path, config ?? { checks: [] }),\n    [path, config, validateField],\n  );\n\n  const touch = useCallback(() => touchField(path), [path, touchField]);\n  const clear = useCallback(() => clearField(path), [path, clearField]);\n\n  return {\n    state,\n    validate,\n    touch,\n    clear,\n    errors: state.result?.errors ?? [],\n    isValid: state.result?.valid ?? true,\n  };\n}\n"
  },
  {
    "path": "packages/react-email/src/contexts/visibility.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface VisibilityContextValue {\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  ctx: CoreVisibilityContext;\n}\n\nconst VisibilityContext = createContext<VisibilityContextValue | null>(null);\n\nexport interface VisibilityProviderProps {\n  children: ReactNode;\n}\n\nexport function VisibilityProvider({ children }: VisibilityProviderProps) {\n  const { state } = useStateStore();\n\n  const ctx: CoreVisibilityContext = useMemo(\n    () => ({ stateModel: state }),\n    [state],\n  );\n\n  const isVisible = useMemo(\n    () => (condition: VisibilityCondition | undefined) =>\n      evaluateVisibility(condition, ctx),\n    [ctx],\n  );\n\n  const value = useMemo<VisibilityContextValue>(\n    () => ({ isVisible, ctx }),\n    [isVisible, ctx],\n  );\n\n  return (\n    <VisibilityContext.Provider value={value}>\n      {children}\n    </VisibilityContext.Provider>\n  );\n}\n\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = useContext(VisibilityContext);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): boolean {\n  const { isVisible } = useVisibility();\n  return isVisible(condition);\n}\n"
  },
  {
    "path": "packages/react-email/src/index.ts",
    "content": "// Schema\nexport { schema, type ReactEmailSchema, type ReactEmailSpec } from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec } from \"@json-render/core\";\n\n// Catalog-aware types\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n\n// Contexts\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./contexts/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n  type VisibilityProviderProps,\n} from \"./contexts/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./contexts/actions\";\n\nexport {\n  ValidationProvider,\n  useValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./contexts/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/repeat-scope\";\n\n// Renderer\nexport {\n  defineRegistry,\n  type DefineRegistryResult,\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRenderer,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n} from \"./renderer\";\n\n// Standard components\nexport { standardComponents } from \"./components\";\n\n// Server-side render functions\nexport { renderToHtml, renderToPlainText, type RenderOptions } from \"./render\";\n\n// Catalog definitions\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/react-email/src/render.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport type { Spec } from \"@json-render/core\";\nimport { renderToHtml, renderToPlainText } from \"./render\";\nimport { examples } from \"./__fixtures__/examples\";\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\n/** Minimal valid email spec — Html > Head + Body */\nfunction minimalSpec(bodyChildren: Spec[\"elements\"] = {}): Spec {\n  return {\n    root: \"html\",\n    elements: {\n      html: {\n        type: \"Html\",\n        props: { lang: \"en\", dir: null },\n        children: [\"head\", \"body\"],\n      },\n      head: { type: \"Head\", props: {}, children: [] },\n      body: {\n        type: \"Body\",\n        props: { style: null },\n        children: Object.keys(bodyChildren),\n      },\n      ...bodyChildren,\n    },\n  };\n}\n\nfunction validateSpecStructure(spec: Spec) {\n  const errors: string[] = [];\n\n  if (!spec.elements[spec.root]) {\n    errors.push(`Root element \"${spec.root}\" not found in elements`);\n  }\n\n  for (const [id, element] of Object.entries(spec.elements)) {\n    if (element.children) {\n      for (const childId of element.children) {\n        if (!spec.elements[childId]) {\n          errors.push(\n            `Element \"${id}\" references child \"${childId}\" which does not exist`,\n          );\n        }\n      }\n    }\n  }\n\n  const referencedIds = new Set<string>();\n  referencedIds.add(spec.root);\n  for (const element of Object.values(spec.elements)) {\n    if (element.children) {\n      for (const childId of element.children) {\n        referencedIds.add(childId);\n      }\n    }\n  }\n  for (const id of Object.keys(spec.elements)) {\n    if (!referencedIds.has(id)) {\n      errors.push(`Element \"${id}\" is orphaned (not referenced by any parent)`);\n    }\n  }\n\n  return errors;\n}\n\n// =============================================================================\n// renderToHtml\n// =============================================================================\n\ndescribe(\"renderToHtml\", () => {\n  it(\"renders a minimal spec to valid HTML\", async () => {\n    const html = await renderToHtml(minimalSpec());\n    expect(html).toContain(\"<!DOCTYPE\");\n    expect(html).toContain(\"<html\");\n    expect(html).toContain(\"</html>\");\n  });\n\n  it(\"renders Text component content\", async () => {\n    const spec = minimalSpec({\n      text: {\n        type: \"Text\",\n        props: { text: \"Hello World\", style: null },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"Hello World\");\n  });\n\n  it(\"renders Heading with correct tag level\", async () => {\n    const spec = minimalSpec({\n      heading: {\n        type: \"Heading\",\n        props: { text: \"Title\", as: \"h1\", style: null },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"<h1\");\n    expect(html).toContain(\"Title\");\n  });\n\n  it(\"renders Link with href\", async () => {\n    const spec = minimalSpec({\n      link: {\n        type: \"Link\",\n        props: {\n          text: \"Click here\",\n          href: \"https://example.com\",\n          style: null,\n        },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"https://example.com\");\n    expect(html).toContain(\"Click here\");\n  });\n\n  it(\"renders Button with href and text\", async () => {\n    const spec = minimalSpec({\n      btn: {\n        type: \"Button\",\n        props: {\n          text: \"Get Started\",\n          href: \"https://example.com/start\",\n          style: null,\n        },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"https://example.com/start\");\n    expect(html).toContain(\"Get Started\");\n  });\n\n  it(\"renders Image with src and alt\", async () => {\n    const spec = minimalSpec({\n      img: {\n        type: \"Image\",\n        props: {\n          src: \"https://example.com/logo.png\",\n          alt: \"Logo\",\n          width: 100,\n          height: 50,\n          style: null,\n        },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"https://example.com/logo.png\");\n    expect(html).toContain('alt=\"Logo\"');\n  });\n\n  it(\"renders Hr element\", async () => {\n    const spec = minimalSpec({\n      divider: {\n        type: \"Hr\",\n        props: { style: { borderColor: \"#eaeaea\" } },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"<hr\");\n  });\n\n  it(\"renders Preview text\", async () => {\n    const spec: Spec = {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"preview\", \"body\"],\n        },\n        head: { type: \"Head\", props: {}, children: [] },\n        preview: {\n          type: \"Preview\",\n          props: { text: \"Inbox preview text\" },\n          children: [],\n        },\n        body: { type: \"Body\", props: { style: null }, children: [] },\n      },\n    };\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"Inbox preview text\");\n  });\n\n  it(\"renders nested Container > Section > Text\", async () => {\n    const spec = minimalSpec({\n      container: {\n        type: \"Container\",\n        props: { style: { maxWidth: \"600px\" } },\n        children: [\"section\"],\n      },\n      section: {\n        type: \"Section\",\n        props: { style: null },\n        children: [\"text\"],\n      },\n      text: {\n        type: \"Text\",\n        props: { text: \"Nested content\", style: null },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"Nested content\");\n  });\n\n  it(\"renders Row and Column layout\", async () => {\n    const spec = minimalSpec({\n      section: {\n        type: \"Section\",\n        props: { style: null },\n        children: [\"row\"],\n      },\n      row: {\n        type: \"Row\",\n        props: { style: null },\n        children: [\"col1\", \"col2\"],\n      },\n      col1: {\n        type: \"Column\",\n        props: { style: { width: \"50%\" } },\n        children: [\"left-text\"],\n      },\n      col2: {\n        type: \"Column\",\n        props: { style: { width: \"50%\" } },\n        children: [\"right-text\"],\n      },\n      \"left-text\": {\n        type: \"Text\",\n        props: { text: \"Left side\", style: null },\n        children: [],\n      },\n      \"right-text\": {\n        type: \"Text\",\n        props: { text: \"Right side\", style: null },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"Left side\");\n    expect(html).toContain(\"Right side\");\n  });\n\n  it(\"renders Markdown content\", async () => {\n    const spec = minimalSpec({\n      md: {\n        type: \"Markdown\",\n        props: {\n          content: \"# Hello\\n\\nThis is **bold** text.\",\n          markdownContainerStyles: null,\n          markdownCustomStyles: null,\n        },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"Hello\");\n    expect(html).toContain(\"bold\");\n  });\n\n  it(\"applies inline styles\", async () => {\n    const spec = minimalSpec({\n      text: {\n        type: \"Text\",\n        props: {\n          text: \"Styled\",\n          style: { color: \"#ff0000\", fontSize: \"20px\" },\n        },\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"color:#ff0000\");\n    expect(html).toContain(\"font-size:20px\");\n  });\n\n  it(\"skips unknown component types gracefully\", async () => {\n    const spec = minimalSpec({\n      unknown: {\n        type: \"NonExistentComponent\",\n        props: {},\n        children: [],\n      },\n    });\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"<!DOCTYPE\");\n  });\n\n  it(\"handles missing child elements gracefully\", async () => {\n    const spec: Spec = {\n      root: \"html\",\n      elements: {\n        html: {\n          type: \"Html\",\n          props: { lang: \"en\", dir: null },\n          children: [\"head\", \"body\"],\n        },\n        head: { type: \"Head\", props: {}, children: [] },\n        body: {\n          type: \"Body\",\n          props: { style: null },\n          children: [\"missing-child\"],\n        },\n      },\n    };\n    const html = await renderToHtml(spec);\n    expect(html).toContain(\"<!DOCTYPE\");\n  });\n});\n\n// =============================================================================\n// renderToPlainText\n// =============================================================================\n\ndescribe(\"renderToPlainText\", () => {\n  it(\"extracts text content from spec\", async () => {\n    const spec = minimalSpec({\n      text: {\n        type: \"Text\",\n        props: { text: \"Plain text content\", style: null },\n        children: [],\n      },\n    });\n    const text = await renderToPlainText(spec);\n    expect(text).toContain(\"Plain text content\");\n  });\n\n  it(\"extracts link text and URL\", async () => {\n    const spec = minimalSpec({\n      link: {\n        type: \"Link\",\n        props: {\n          text: \"Visit site\",\n          href: \"https://example.com\",\n          style: null,\n        },\n        children: [],\n      },\n    });\n    const text = await renderToPlainText(spec);\n    expect(text).toContain(\"Visit site\");\n    expect(text).toContain(\"https://example.com\");\n  });\n\n  it(\"returns string for minimal spec\", async () => {\n    const text = await renderToPlainText(minimalSpec());\n    expect(typeof text).toBe(\"string\");\n  });\n});\n\n// =============================================================================\n// Example specs — structural validation + render smoke tests\n// =============================================================================\n\ndescribe(\"example specs\", () => {\n  it(\"has at least one example\", () => {\n    expect(examples.length).toBeGreaterThan(0);\n  });\n\n  for (const example of examples) {\n    describe(example.name, () => {\n      it(\"has required metadata\", () => {\n        expect(example.name).toBeTruthy();\n        expect(example.label).toBeTruthy();\n        expect(example.description).toBeTruthy();\n      });\n\n      it(\"has a valid spec structure (no dangling children, no orphans)\", () => {\n        const errors = validateSpecStructure(example.spec);\n        expect(errors).toEqual([]);\n      });\n\n      it(\"root element is Html with Head and Body children\", () => {\n        const root = example.spec.elements[example.spec.root];\n        expect(root).toBeDefined();\n        expect(root!.type).toBe(\"Html\");\n\n        const childTypes = root!.children?.map(\n          (id) => example.spec.elements[id]?.type,\n        );\n        expect(childTypes).toContain(\"Head\");\n        expect(childTypes).toContain(\"Body\");\n      });\n\n      it(\"renders to HTML without errors\", async () => {\n        const html = await renderToHtml(example.spec);\n        expect(html).toBeTruthy();\n        expect(html).toContain(\"<!DOCTYPE\");\n        expect(html).toContain(\"</html>\");\n      });\n\n      it(\"renders to plain text without errors\", async () => {\n        const text = await renderToPlainText(example.spec);\n        expect(text).toBeTruthy();\n        expect(typeof text).toBe(\"string\");\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "packages/react-email/src/render.tsx",
    "content": "import React from \"react\";\nimport { render } from \"@react-email/render\";\nimport type { Spec, UIElement } from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n} from \"@json-render/core\";\nimport { standardComponents } from \"./components/standard\";\n\n// Re-export the standard components for use in custom registries\nexport { standardComponents };\n\nexport type RenderComponentRegistry = Record<string, React.ComponentType<any>>;\n\nexport interface RenderOptions {\n  registry?: RenderComponentRegistry;\n  includeStandard?: boolean;\n  state?: Record<string, unknown>;\n}\n\nconst noopEmit = () => {};\n\nfunction renderElement(\n  elementKey: string,\n  spec: Spec,\n  registry: RenderComponentRegistry,\n  stateModel: Record<string, unknown>,\n  repeatItem?: unknown,\n  repeatIndex?: number,\n  repeatBasePath?: string,\n): React.ReactElement | null {\n  const element = spec.elements[elementKey];\n  if (!element) return null;\n\n  const ctx: PropResolutionContext = {\n    stateModel,\n    repeatItem,\n    repeatIndex,\n    repeatBasePath,\n  };\n\n  if (element.visible !== undefined) {\n    if (!evaluateVisibility(element.visible, ctx)) {\n      return null;\n    }\n  }\n\n  const resolvedProps = resolveElementProps(\n    element.props as Record<string, unknown>,\n    ctx,\n  );\n  const resolvedElement: UIElement = { ...element, props: resolvedProps };\n\n  const Component = registry[resolvedElement.type];\n  if (!Component) return null;\n\n  if (resolvedElement.repeat) {\n    const items =\n      (getByPath(stateModel, resolvedElement.repeat.statePath) as\n        | unknown[]\n        | undefined) ?? [];\n\n    const repeat = resolvedElement.repeat!;\n    const fragments = items.map((item, index) => {\n      const repeatKey = repeat.key;\n      const key =\n        repeatKey && typeof item === \"object\" && item !== null\n          ? String((item as Record<string, unknown>)[repeatKey] ?? index)\n          : String(index);\n\n      const childPath = `${repeat.statePath}/${index}`;\n      const children = resolvedElement.children?.map((childKey) =>\n        renderElement(\n          childKey,\n          spec,\n          registry,\n          stateModel,\n          item,\n          index,\n          childPath,\n        ),\n      );\n\n      return (\n        <Component key={key} element={resolvedElement} emit={noopEmit}>\n          {children}\n        </Component>\n      );\n    });\n\n    return <>{fragments}</>;\n  }\n\n  const children = resolvedElement.children?.map((childKey) =>\n    renderElement(\n      childKey,\n      spec,\n      registry,\n      stateModel,\n      repeatItem,\n      repeatIndex,\n      repeatBasePath,\n    ),\n  );\n\n  return (\n    <Component key={elementKey} element={resolvedElement} emit={noopEmit}>\n      {children && children.length > 0 ? children : undefined}\n    </Component>\n  );\n}\n\nfunction buildDocument(\n  spec: Spec,\n  options: RenderOptions = {},\n): React.ReactElement {\n  const {\n    registry: customRegistry,\n    includeStandard = true,\n    state = {},\n  } = options;\n\n  const mergedState: Record<string, unknown> = {\n    ...spec.state,\n    ...state,\n  };\n\n  const registry: RenderComponentRegistry = {\n    ...(includeStandard ? standardComponents : {}),\n    ...customRegistry,\n  };\n\n  const root = renderElement(spec.root, spec, registry, mergedState);\n  if (!root) {\n    console.warn(\n      `[json-render/react-email] Root element \"${spec.root}\" not found in spec.elements`,\n    );\n  }\n  return root ?? <></>;\n}\n\n/**\n * Render a json-render spec to an HTML email string.\n *\n * This is a standalone server-side function that resolves the spec tree\n * without React hooks or contexts, making it safe to import in Next.js\n * route handlers and other server-only environments.\n */\nexport async function renderToHtml(\n  spec: Spec,\n  options?: RenderOptions,\n): Promise<string> {\n  const document = buildDocument(spec, options);\n  return render(document);\n}\n\n/**\n * Render a json-render spec to a plain text email string.\n */\nexport async function renderToPlainText(\n  spec: Spec,\n  options?: RenderOptions,\n): Promise<string> {\n  const document = buildDocument(spec, options);\n  return render(document, { plainText: true });\n}\n"
  },
  {
    "path": "packages/react-email/src/renderer.tsx",
    "content": "import React, {\n  type ComponentType,\n  type ErrorInfo,\n  type ReactNode,\n  useCallback,\n  useMemo,\n} from \"react\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  SchemaDefinition,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport type { Components, SetState, StateModel } from \"./catalog-types\";\nimport { useIsVisible, useVisibility } from \"./contexts/visibility\";\nimport { useActions } from \"./contexts/actions\";\nimport { useStateStore } from \"./contexts/state\";\nimport { StateProvider } from \"./contexts/state\";\nimport { VisibilityProvider } from \"./contexts/visibility\";\nimport { ActionProvider } from \"./contexts/actions\";\nimport { ValidationProvider } from \"./contexts/validation\";\nimport { standardComponents } from \"./components/standard\";\nimport { RepeatScopeProvider, useRepeatScope } from \"./contexts/repeat-scope\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  element: UIElement<string, P>;\n  children?: ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\nexport type ComponentRenderer<P = Record<string, unknown>> = ComponentType<\n  ComponentRenderProps<P>\n>;\n\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\nexport interface RendererProps {\n  spec: Spec | null;\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\n// =============================================================================\n// ElementErrorBoundary\n// =============================================================================\n\ninterface ElementErrorBoundaryProps {\n  elementType: string;\n  children: ReactNode;\n}\n\ninterface ElementErrorBoundaryState {\n  hasError: boolean;\n}\n\nclass ElementErrorBoundary extends React.Component<\n  ElementErrorBoundaryProps,\n  ElementErrorBoundaryState\n> {\n  constructor(props: ElementErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ElementErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    console.error(\n      `[json-render/react-email] Rendering error in <${this.props.elementType}>:`,\n      error,\n      info.componentStack,\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return null;\n    }\n    return this.props.children;\n  }\n}\n\n// =============================================================================\n// ElementRenderer\n// =============================================================================\n\ninterface ElementRendererProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\nconst ElementRenderer = React.memo(function ElementRenderer({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: ElementRendererProps) {\n  const repeatScope = useRepeatScope();\n  const { ctx } = useVisibility();\n  const { execute } = useActions();\n\n  const fullCtx: PropResolutionContext = useMemo(\n    () =>\n      repeatScope\n        ? {\n            ...ctx,\n            repeatItem: repeatScope.item,\n            repeatIndex: repeatScope.index,\n            repeatBasePath: repeatScope.basePath,\n          }\n        : ctx,\n    [ctx, repeatScope],\n  );\n\n  const isVisible =\n    element.visible === undefined\n      ? true\n      : evaluateVisibility(element.visible, fullCtx);\n\n  const onBindings = element.on;\n  const emit = useCallback(\n    (eventName: string) => {\n      const binding = onBindings?.[eventName];\n      if (!binding) return;\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      for (const b of actionBindings) {\n        if (!b.params) {\n          execute(b);\n          continue;\n        }\n        const resolved: Record<string, unknown> = {};\n        for (const [key, val] of Object.entries(b.params)) {\n          resolved[key] = resolveActionParam(val, fullCtx);\n        }\n        execute({ ...b, params: resolved });\n      }\n    },\n    [onBindings, execute, fullCtx],\n  );\n\n  if (!isVisible) {\n    return null;\n  }\n\n  const rawProps = element.props as Record<string, unknown>;\n  const elementBindings = resolveBindings(rawProps, fullCtx);\n  const resolvedProps = resolveElementProps(rawProps, fullCtx);\n\n  const resolvedElement =\n    resolvedProps !== element.props\n      ? { ...element, props: resolvedProps }\n      : element;\n\n  const Component = registry[resolvedElement.type] ?? fallback;\n\n  if (!Component) {\n    console.warn(\n      `[json-render/react-email] No renderer for component type: ${resolvedElement.type}`,\n    );\n    return null;\n  }\n\n  const children = resolvedElement.repeat ? (\n    <RepeatChildren\n      element={resolvedElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  ) : (\n    resolvedElement.children?.map((childKey) => {\n      const childElement = spec.elements[childKey];\n      if (!childElement) {\n        if (!loading) {\n          console.warn(\n            `[json-render/react-email] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\".`,\n          );\n        }\n        return null;\n      }\n      return (\n        <ElementRenderer\n          key={childKey}\n          element={childElement}\n          spec={spec}\n          registry={registry}\n          loading={loading}\n          fallback={fallback}\n        />\n      );\n    })\n  );\n\n  return (\n    <ElementErrorBoundary elementType={resolvedElement.type}>\n      <Component\n        element={resolvedElement}\n        emit={emit}\n        bindings={elementBindings}\n        loading={loading}\n      >\n        {children}\n      </Component>\n    </ElementErrorBoundary>\n  );\n});\n\n// =============================================================================\n// RepeatChildren\n// =============================================================================\n\nfunction RepeatChildren({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}) {\n  const { state } = useStateStore();\n  const repeat = element.repeat!;\n  const statePath = repeat.statePath;\n\n  const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];\n\n  return (\n    <>\n      {items.map((itemValue, index) => {\n        const key =\n          repeat.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String(\n                (itemValue as Record<string, unknown>)[repeat.key] ?? index,\n              )\n            : String(index);\n\n        return (\n          <RepeatScopeProvider\n            key={key}\n            item={itemValue}\n            index={index}\n            basePath={`${statePath}/${index}`}\n          >\n            {element.children?.map((childKey) => {\n              const childElement = spec.elements[childKey];\n              if (!childElement) {\n                if (!loading) {\n                  console.warn(\n                    `[json-render/react-email] Missing element \"${childKey}\" referenced as child of \"${element.type}\" (repeat).`,\n                  );\n                }\n                return null;\n              }\n              return (\n                <ElementRenderer\n                  key={childKey}\n                  element={childElement}\n                  spec={spec}\n                  registry={registry}\n                  loading={loading}\n                  fallback={fallback}\n                />\n              );\n            })}\n          </RepeatScopeProvider>\n        );\n      })}\n    </>\n  );\n}\n\n// =============================================================================\n// Renderer\n// =============================================================================\n\nexport function Renderer({\n  spec,\n  registry: customRegistry,\n  includeStandard = true,\n  loading,\n  fallback,\n}: RendererProps) {\n  const registry: ComponentRegistry = useMemo(\n    () => ({\n      ...(includeStandard ? standardComponents : {}),\n      ...customRegistry,\n    }),\n    [customRegistry, includeStandard],\n  );\n\n  if (!spec || !spec.root) {\n    return null;\n  }\n\n  const rootElement = spec.elements[spec.root];\n  if (!rootElement) {\n    return null;\n  }\n\n  return (\n    <ElementRenderer\n      element={rootElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  );\n}\n\n// =============================================================================\n// JSONUIProvider\n// =============================================================================\n\nexport interface JSONUIProviderProps {\n  initialState?: Record<string, unknown>;\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  navigate?: (path: string) => void;\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  onStateChange?: (path: string, value: unknown) => void;\n  children: ReactNode;\n}\n\nexport function JSONUIProvider({\n  initialState,\n  handlers,\n  navigate,\n  validationFunctions,\n  onStateChange,\n  children,\n}: JSONUIProviderProps) {\n  return (\n    <StateProvider initialState={initialState} onStateChange={onStateChange}>\n      <VisibilityProvider>\n        <ActionProvider handlers={handlers} navigate={navigate}>\n          <ValidationProvider customFunctions={validationFunctions}>\n            {children}\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n// =============================================================================\n// defineRegistry\n// =============================================================================\n\nexport interface DefineRegistryResult {\n  registry: ComponentRegistry;\n}\n\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: React.ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => React.ReactNode;\n\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: {\n    components?: Components<C>;\n  },\n): DefineRegistryResult {\n  const registry: ComponentRegistry = {};\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = ({\n        element,\n        children,\n        emit,\n        bindings,\n        loading,\n      }: ComponentRenderProps) => {\n        return (componentFn as DefineRegistryComponentFn)({\n          props: element.props,\n          children,\n          emit,\n          bindings,\n          loading,\n        });\n      };\n    }\n  }\n\n  return { registry };\n}\n\n// =============================================================================\n// createRenderer\n// =============================================================================\n\nexport interface CreateRendererProps {\n  spec: Spec | null;\n  state?: Record<string, unknown>;\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  onStateChange?: (path: string, value: unknown) => void;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: ComponentType<\n    ComponentRenderProps<\n      TComponents[K][\"props\"] extends { _output: infer O }\n        ? O\n        : Record<string, unknown>\n    >\n  >;\n};\n\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentType<CreateRendererProps> {\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  return function CatalogRenderer({\n    spec,\n    state,\n    onAction,\n    onStateChange,\n    loading,\n    fallback,\n  }: CreateRendererProps) {\n    const actionHandlers = onAction\n      ? new Proxy(\n          {} as Record<\n            string,\n            (params: Record<string, unknown>) => void | Promise<void>\n          >,\n          {\n            get: (_target, prop: string) => {\n              return (params: Record<string, unknown>) =>\n                onAction(prop, params);\n            },\n            has: () => true,\n          },\n        )\n      : undefined;\n\n    return (\n      <StateProvider initialState={state} onStateChange={onStateChange}>\n        <VisibilityProvider>\n          <ActionProvider handlers={actionHandlers}>\n            <ValidationProvider>\n              <Renderer\n                spec={spec}\n                registry={registry}\n                loading={loading}\n                fallback={fallback}\n              />\n            </ValidationProvider>\n          </ActionProvider>\n        </VisibilityProvider>\n      </StateProvider>\n    );\n  };\n}\n"
  },
  {
    "path": "packages/react-email/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/react-email\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas\n *\n * Reuses the same { root, elements } spec format as the React and React Native renderers.\n */\nexport const schema = defineSchema(\n  (s) => ({\n    spec: s.object({\n      root: s.string(),\n      elements: s.record(\n        s.object({\n          type: s.ref(\"catalog.components\"),\n          props: s.propsOf(\"catalog.components\"),\n          children: s.array(s.string()),\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    catalog: s.object({\n      components: s.map({\n        props: s.zod(),\n        slots: s.array(s.string()),\n        description: s.string(),\n        example: s.any(),\n      }),\n    }),\n  }),\n  {\n    defaultRules: [\n      \"The root element MUST be an Html component. Its children MUST include Head and Body components.\",\n      \"Body should contain a Container component to constrain width (typically 600px max for email clients).\",\n      \"All styles MUST be inline. Email clients strip <style> tags, so every component that accepts a style prop should use it for visual customization.\",\n      \"Image src must be a fully qualified URL (absolute, not relative). For placeholder images use https://picsum.photos/{width}/{height}?random={n}.\",\n      \"Emails are static documents. There are no interactive actions or form inputs.\",\n      \"Use Section, Row, and Column for layout. These map to table-based email structures for maximum compatibility.\",\n      \"Use Preview to set the preview text shown in email client inboxes before opening.\",\n      \"Use Heading (h1-h6) and Text for all text content. Raw strings are not supported.\",\n      \"Button renders as a link styled as a button. Always provide both text and href.\",\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist.\",\n    ],\n  },\n);\n\nexport type ReactEmailSchema = typeof schema;\n\nexport type ReactEmailSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/react-email/src/server.ts",
    "content": "// Server-safe entry point: schema and catalog definitions only.\n// Does not import React or @react-email/components.\n\nexport { schema, type ReactEmailSchema, type ReactEmailSpec } from \"./schema\";\n\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n\nexport type { Spec } from \"@json-render/core\";\n\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n"
  },
  {
    "path": "packages/react-email/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-email/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/server.ts\", \"src/catalog.ts\", \"src/render.tsx\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"react\",\n    \"@json-render/core\",\n    \"@react-email/components\",\n    \"@react-email/render\",\n    \"zod\",\n  ],\n});\n"
  },
  {
    "path": "packages/react-native/CHANGELOG.md",
    "content": "# @json-render/react-native\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- b103676: Fix install failure caused by `@internal/react-state` (a private workspace package) being listed as a published dependency. The internal package is now bundled into each renderer's output at build time, so consumers no longer need to resolve it from npm.\n  - @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @internal/react-state@0.8.1\n\n## 0.8.0\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n\n## 0.7.0\n\n### Patch Changes\n\n- Updated dependencies [2d70fab]\n  - @json-render/core@0.7.0\n\n## 0.6.1\n\n### Patch Changes\n\n- @json-render/core@0.6.1\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [ea97aff]\n  - @json-render/core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 06b8745: Chat mode (inline GenUI), AI SDK integration, two-way binding, and expression-based visibility/props.\n\n  ### New: Chat Mode (Inline GenUI)\n\n  Two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets AI respond conversationally with embedded UI specs — ideal for chatbots and copilot experiences.\n  - `catalog.prompt({ mode: \"chat\" })` generates a chat-aware system prompt\n  - `pipeJsonRender()` server-side transform separates text from JSONL patches in a mixed stream\n  - `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n\n  ### New: AI SDK Integration\n\n  First-class Vercel AI SDK support with typed data parts and stream utilities.\n  - `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n  - `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n  - `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n  ### New: React Chat Hooks\n  - `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n  - `useJsonRenderMessage()` — extract spec + text from a message's parts array\n  - `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n  - `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem` expressions\n\n  ### New: Two-Way Binding\n\n  Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state.\n\n  ### New: Expression-Based Props and Visibility\n\n  Replaced string token rewriting with structured expression objects:\n  - Props: `{ $state: \"/path\" }`, `{ $item: \"field\" }`, `{ $index: true }`\n  - Visibility: `{ $state: \"/path\", eq: \"value\" }`, `{ $item: \"active\" }`, `{ $index: true, gt: 0 }`\n  - Logic: `{ $and: [...] }`, `{ $or: [...] }`, and implicit AND via arrays\n  - Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`\n\n  ### New: Utilities\n  - `applySpecPatch()` — typed convenience wrapper for applying a single patch to a Spec\n  - `nestedToFlat()` — convert nested tree specs to flat `{ root, elements }` format\n  - `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n  ### New: Chat Example\n\n  Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming UI generation.\n\n  ### Improved: Renderer\n  - `ElementRenderer` is now `React.memo`'d for better performance in repeat lists\n  - `emit` is always defined (never `undefined`) — no more optional chaining needed\n  - Action params are resolved through `resolveActionParam` supporting `$item`, `$index`, `$state`\n  - Repeat scope now passes the actual item object instead of requiring token rewriting\n\n  ### Breaking Changes\n  - **Expressions renamed**: `{ $path }` / `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }`\n  - **Visibility conditions**: `{ path }` → `{ $state }`, `{ and/or/not }` → `{ $and/$or }` with `not` as operator flag\n  - **DynamicValue**: `{ path: string }` → `{ $state: string }`\n  - **Repeat field**: `repeat.path` → `repeat.statePath`\n  - **Action params**: `path` → `statePath` in setState action params\n  - **Provider props**: `actionHandlers` → `handlers` on `JSONUIProvider`/`ActionProvider`\n  - **Auth removed**: `AuthState` type and `{ auth }` visibility conditions removed — model auth as regular state\n  - **Legacy catalog removed**: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`, `ComponentDefinition`, `CatalogConfig`, `SystemPromptOptions` removed\n  - **React exports removed**: `createRendererFromCatalog`, `rewriteRepeatTokens`\n  - **Codegen**: `traverseTree` → `traverseSpec`, `SpecVisitor` → `TreeVisitor`\n\n### Patch Changes\n\n- Updated dependencies [06b8745]\n  - @json-render/core@0.6.0\n\n## 0.5.2\n\n### Patch Changes\n\n- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.\n- Updated dependencies [429e456]\n  - @json-render/core@0.5.2\n\n## 0.5.1\n\n### Patch Changes\n\n- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.\n  - @json-render/core@0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.\n\n### Patch Changes\n\n- Updated dependencies [3d2d1ad]\n  - @json-render/core@0.5.0\n"
  },
  {
    "path": "packages/react-native/README.md",
    "content": "# @json-render/react-native\n\nReact Native renderer for json-render. Turn JSON specs into native mobile UIs with standard components, data binding, visibility, actions, and dynamic props.\n\n## Installation\n\n```bash\nnpm install @json-render/react-native @json-render/core zod\n```\n\n## Quick Start\n\n### 1. Create a Catalog\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 { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    // Add custom components\n    Icon: {\n      props: z.object({\n        name: z.string(),\n        size: z.number().nullable(),\n        color: z.string().nullable(),\n      }),\n      slots: [],\n      description: \"Icon display using Ionicons\",\n    },\n  },\n  actions: standardActionDefinitions,\n});\n```\n\n### 2. Define Custom Component Implementations\n\n```tsx\nimport { defineRegistry, type Components } from \"@json-render/react-native\";\nimport Ionicons from \"@expo/vector-icons/Ionicons\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Icon: ({ props }) => (\n      <Ionicons\n        name={props.name as keyof typeof Ionicons.glyphMap}\n        size={props.size ?? 24}\n        color={props.color ?? \"#111827\"}\n      />\n    ),\n  } as Components<typeof catalog>,\n});\n```\n\nStandard components (Container, Row, Column, Button, TextInput, etc.) are included by default. You only need to register custom ones.\n\n### 3. Render Specs\n\n```tsx\nimport {\n  Renderer,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n  ValidationProvider,\n} from \"@json-render/react-native\";\nimport { registry } from \"./registry\";\n\nfunction App({ spec }) {\n  return (\n    <StateProvider initialState={{}}>\n      <VisibilityProvider>\n        <ActionProvider handlers={{}}>\n          <ValidationProvider>\n            <Renderer spec={spec} registry={registry} />\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Standard Components\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Container` | Basic wrapper with padding, background, border radius |\n| `Row` | Horizontal flex layout with gap, alignment, flex |\n| `Column` | Vertical flex layout with gap, alignment, flex |\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 between elements |\n| `Divider` | Thin line separator |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\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\n| Component | Description |\n|-----------|-------------|\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\n| Component | Description |\n|-----------|-------------|\n| `Spinner` | Loading indicator |\n| `ProgressBar` | Progress indicator |\n\n### Composite\n\n| Component | Description |\n|-----------|-------------|\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\nElements can use `visible` to show/hide based on state. Same syntax as [@json-render/react](../react/README.md#visibility-conditions): `{ \"$state\": \"/path\" }`, `{ \"$state\": \"/path\", \"eq\": value }`, `{ \"$state\": \"/path\", \"not\": true }`, or `[ cond1, cond2 ]` for AND.\n\n## Pressable Component\n\nThe `Pressable` component wraps children and triggers an action on press. It's essential for building 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-tab-icon\", \"home-tab-label\"]\n}\n```\n\n## Built-in Actions\n\nThe `setState` action is handled automatically by `ActionProvider`. It updates the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:\n\n```json\n{\n  \"action\": \"setState\",\n  \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n}\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can be a dynamic expression resolved at render time:\n\n```json\n{\n  \"type\": \"Icon\",\n  \"props\": {\n    \"name\": {\n      \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n      \"$then\": \"home\",\n      \"$else\": \"home-outline\"\n    },\n    \"color\": {\n      \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"home\" },\n      \"$then\": \"#007AFF\",\n      \"$else\": \"#8E8E93\"\n    }\n  }\n}\n```\n\nSee [@json-render/core](../core/README.md) for full expression syntax.\n\n## Tab Navigation Pattern\n\nCombine `Pressable`, `setState`, visibility conditions, and dynamic props for functional tabs:\n\n1. Each tab button is a `Pressable` with `action: \"setState\"` and `actionParams: { statePath: \"/activeTab\", value: \"tabName\" }`\n2. Tab icons/labels use `$cond` dynamic props for active/inactive styling\n3. Tab content sections use `visible` conditions: `{ \"$state\": \"/activeTab\", \"eq\": \"tabName\" }`\n\n## AI Prompt Generation\n\n```typescript\nconst systemPrompt = catalog.prompt({\n  customRules: [\n    \"Use SafeArea as the root element\",\n    \"Use Pressable + setState for interactive tabs\",\n  ],\n});\n```\n\n## External Store (Controlled Mode)\n\nFor full control over state, pass a `StateStore` to bypass the internal state and wire json-render to any state management library (Redux, Zustand, XState, etc.):\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-native\";\n\n// Use the built-in store outside of React\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>\n  {children}\n</StateProvider>\n\n// Mutate from anywhere — React will re-render automatically:\nstore.set(\"/count\", 1);\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored. The store is the single source of truth. The same `store` prop is available on `createRenderer`, `JSONUIProvider`, and `StateProvider`.\n\n## Hooks\n\n| Hook | Purpose |\n|------|---------|\n| `useStateStore()` | Access state context (`state`, `get`, `set`, `update`) |\n| `useStateValue(path)` | Get single value from state |\n| `useStateBinding(path)` | Two-way data binding (returns `[value, setValue]`) |\n| `useVisibility()` | Access visibility evaluation |\n| `useIsVisible(condition)` | Check if condition is met |\n| `useActions()` | Access action context |\n| `useAction(name)` | Get a single action dispatch function |\n| `useUIStream(options)` | Stream specs from an API endpoint |\n| `createStandardActionHandlers(options)` | Create handlers for standard actions |\n| `createStateStore(initialState)` | Create a framework-agnostic in-memory `StateStore` |\n"
  },
  {
    "path": "packages/react-native/package.json",
    "content": "{\n  \"name\": \"@json-render/react-native\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"React Native renderer for @json-render/core. JSON becomes React Native components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"react-native\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/react-native\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./schema\": {\n      \"types\": \"./dist/schema.d.ts\",\n      \"import\": \"./dist/schema.mjs\",\n      \"require\": \"./dist/schema.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/react-state\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"react-native\": \"0.83.1\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^18.0.0 || ^19.0.0\",\n    \"react-native\": \">=0.71.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-native/src/catalog-types.ts",
    "content": "import type { ReactNode } from \"react\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\n/**\n * State setter function for updating application state\n */\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\n/**\n * Context passed to component render functions\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => {\n *   return <Pressable onPress={() => ctx.emit(\"press\")}><Text>{ctx.props.label}</Text></Pressable>\n * }\n */\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> {\n  props: InferComponentProps<C, K>;\n  children?: ReactNode;\n  /** Emit a named event. The renderer resolves the event to an action binding from the element's `on` field. */\n  emit: (event: string) => void;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\n/**\n * Component render function type for React Native\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = ({ props, emit }) => (\n *   <Pressable onPress={() => emit(\"press\")}><Text>{props.label}</Text></Pressable>\n * );\n */\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => ReactNode;\n\n/**\n * Registry of all component render functions for a catalog\n * @example\n * const components: Components<typeof myCatalog> = {\n *   Button: ({ props }) => <Pressable><Text>{props.label}</Text></Pressable>,\n *   Input: ({ props }) => <TextInput placeholder={props.placeholder} />,\n * };\n */\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n\n// =============================================================================\n// Action Types\n// =============================================================================\n\n/**\n * Action handler function type\n * @example\n * const viewCustomers: ActionFn<typeof catalog, 'viewCustomers'> = async (params, setState) => {\n *   const data = await fetch('/api/customers');\n *   setState(prev => ({ ...prev, customers: data }));\n * };\n */\nexport type ActionFn<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = (\n  params: InferActionParams<C, K> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Registry of all action handlers for a catalog\n * @example\n * const actions: Actions<typeof myCatalog> = {\n *   viewCustomers: async (params, setState) => { ... },\n *   createCustomer: async (params, setState) => { ... },\n * };\n */\nexport type Actions<C extends Catalog> = {\n  [K in keyof InferCatalogActions<C>]: ActionFn<C, K>;\n};\n"
  },
  {
    "path": "packages/react-native/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\n// =============================================================================\n// Standard Component Definitions for React Native\n// =============================================================================\n\n/**\n * Standard component definitions for React Native catalogs.\n *\n * These can be used directly or extended with custom components.\n * All components are built using React Native core primitives only.\n */\nexport const standardComponentDefinitions = {\n  // ==========================================================================\n  // Layout Components\n  // ==========================================================================\n\n  Container: {\n    props: z.object({\n      padding: z.number().nullable(),\n      paddingHorizontal: z.number().nullable(),\n      paddingVertical: z.number().nullable(),\n      margin: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n      borderRadius: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Generic container wrapper. Use for grouping elements with padding, margin, and background color.\",\n    example: { padding: 16, backgroundColor: \"#FFFFFF\" },\n  },\n\n  Row: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\", \"baseline\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n          \"space-evenly\",\n        ])\n        .nullable(),\n      flexWrap: z.enum([\"wrap\", \"nowrap\"]).nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Horizontal flex layout. Use for placing elements side by side.\",\n    example: { gap: 12, alignItems: \"center\" },\n  },\n\n  Column: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\", \"baseline\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n          \"space-evenly\",\n        ])\n        .nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Vertical flex layout. Use for stacking elements top to bottom.\",\n    example: { gap: 12, padding: 16 },\n  },\n\n  ScrollContainer: {\n    props: z.object({\n      horizontal: z.boolean().nullable(),\n      showsScrollIndicator: z.boolean().nullable(),\n      padding: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Scrollable container. Use for content that may overflow the screen.\",\n  },\n\n  SafeArea: {\n    props: z.object({\n      backgroundColor: z.string().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Safe area container that respects device notches and system bars. Use as the outermost wrapper for screens.\",\n  },\n\n  Spacer: {\n    props: z.object({\n      size: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Empty space between elements. Set size for fixed spacing or flex for flexible spacing.\",\n  },\n\n  Pressable: {\n    props: z.object({}),\n    events: [\"press\", \"longPress\"],\n    slots: [\"default\"],\n    description:\n      \"Touchable wrapper that triggers events on press. Wrap any element to make it tappable. Bind on.press to setState to update state for visibility-driven UIs like tabs.\",\n  },\n\n  Divider: {\n    props: z.object({\n      direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n      color: z.string().nullable(),\n      thickness: z.number().nullable(),\n      margin: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Thin line separator between content sections.\",\n  },\n\n  // ==========================================================================\n  // Content Components\n  // ==========================================================================\n\n  Heading: {\n    props: z.object({\n      text: z.string(),\n      level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Heading text at various levels. h1 is largest, h4 is smallest.\",\n    example: { text: \"Welcome\", level: \"h1\" },\n  },\n\n  Paragraph: {\n    props: z.object({\n      text: z.string(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n      numberOfLines: z.number().nullable(),\n      fontSize: z.number().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Body text paragraph. Use for descriptions and longer content.\",\n    example: { text: \"This is a paragraph of body text.\" },\n  },\n\n  Label: {\n    props: z.object({\n      text: z.string(),\n      color: z.string().nullable(),\n      bold: z.boolean().nullable(),\n      size: z.enum([\"xs\", \"sm\", \"md\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Small utility text. Use for captions, form labels, and secondary information.\",\n    example: { text: \"Status\", size: \"sm\" },\n  },\n\n  Image: {\n    props: z.object({\n      src: z.string(),\n      alt: z.string().nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      resizeMode: z.enum([\"cover\", \"contain\", \"stretch\", \"center\"]).nullable(),\n      borderRadius: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Image display. Provide a source URL and optional dimensions.\",\n    example: {\n      src: \"https://picsum.photos/300/200\",\n      alt: \"Photo\",\n      width: 300,\n      height: 200,\n    },\n  },\n\n  Avatar: {\n    props: z.object({\n      src: z.string().nullable(),\n      initials: z.string().nullable(),\n      size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Circular avatar showing an image or initials. Use for user profiles and contacts.\",\n    example: { initials: \"JD\", size: \"md\" },\n  },\n\n  Badge: {\n    props: z.object({\n      label: z.string(),\n      variant: z\n        .enum([\"default\", \"info\", \"success\", \"warning\", \"error\"])\n        .nullable(),\n    }),\n    slots: [],\n    description:\n      \"Small colored indicator with a label. Use for status, counts, and categories.\",\n    example: { label: \"New\", variant: \"info\" },\n  },\n\n  Chip: {\n    props: z.object({\n      label: z.string(),\n      selected: z.boolean().nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    events: [\"press\", \"remove\"],\n    slots: [],\n    description:\n      \"Tag or filter chip. Bind on.remove for removable chips, on.press for selectable chips.\",\n  },\n\n  // ==========================================================================\n  // Input Components\n  // ==========================================================================\n\n  Button: {\n    props: z.object({\n      label: z.string(),\n      variant: z\n        .enum([\"primary\", \"secondary\", \"danger\", \"outline\", \"ghost\"])\n        .nullable(),\n      size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      disabled: z.boolean().nullable(),\n      loading: z.boolean().nullable(),\n    }),\n    events: [\"press\"],\n    slots: [],\n    description:\n      \"Pressable button with label. Set variant for styling. Bind on.press for the handler to call on press.\",\n    example: { label: \"Submit\", variant: \"primary\" },\n  },\n\n  TextInput: {\n    props: z.object({\n      placeholder: z.string().nullable(),\n      value: z.string().nullable(),\n      secureTextEntry: z.boolean().nullable(),\n      keyboardType: z\n        .enum([\"default\", \"email-address\", \"numeric\", \"phone-pad\", \"url\"])\n        .nullable(),\n      multiline: z.boolean().nullable(),\n      numberOfLines: z.number().nullable(),\n      label: z.string().nullable(),\n      flex: z.number().nullable(),\n    }),\n    events: [\"submit\", \"focus\", \"blur\"],\n    slots: [],\n    description:\n      \"Text input field. Use $bindState to bind to the state model for two-way binding. The value typed by the user is stored at the bound path.\",\n    example: {\n      placeholder: \"Enter text...\",\n      label: \"Name\",\n    },\n  },\n\n  Switch: {\n    props: z.object({\n      checked: z.boolean().nullable(),\n      label: z.string().nullable(),\n      disabled: z.boolean().nullable(),\n    }),\n    events: [\"change\"],\n    slots: [],\n    description: \"Toggle switch. Use $bindState to bind to the state model.\",\n  },\n\n  Checkbox: {\n    props: z.object({\n      checked: z.boolean().nullable(),\n      label: z.string().nullable(),\n      disabled: z.boolean().nullable(),\n    }),\n    events: [\"change\"],\n    slots: [],\n    description:\n      \"Checkbox for boolean selections. Use $bindState to bind to the state model.\",\n  },\n\n  Slider: {\n    props: z.object({\n      min: z.number().nullable(),\n      max: z.number().nullable(),\n      step: z.number().nullable(),\n      value: z.number().nullable(),\n      label: z.string().nullable(),\n      color: z.string().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Range slider for numeric values. Use $bindState to bind to the state model.\",\n  },\n\n  SearchBar: {\n    props: z.object({\n      placeholder: z.string().nullable(),\n      value: z.string().nullable(),\n    }),\n    events: [\"submit\"],\n    slots: [],\n    description:\n      \"Search input with icon. Bind on.submit to trigger search on submit.\",\n  },\n\n  // ==========================================================================\n  // Feedback Components\n  // ==========================================================================\n\n  Spinner: {\n    props: z.object({\n      size: z.enum([\"small\", \"large\"]).nullable(),\n      color: z.string().nullable(),\n    }),\n    slots: [],\n    description: \"Loading spinner indicator. Use while content is loading.\",\n  },\n\n  ProgressBar: {\n    props: z.object({\n      progress: z.number(),\n      color: z.string().nullable(),\n      trackColor: z.string().nullable(),\n      height: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Horizontal progress bar. Set progress from 0 to 1.\",\n  },\n\n  // ==========================================================================\n  // Composite Components\n  // ==========================================================================\n\n  Card: {\n    props: z.object({\n      title: z.string().nullable(),\n      subtitle: z.string().nullable(),\n      padding: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n      borderRadius: z.number().nullable(),\n      elevated: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Elevated card container with optional title. Use for grouping related content.\",\n    example: { title: \"Details\", padding: 16, elevated: true },\n  },\n\n  ListItem: {\n    props: z.object({\n      title: z.string(),\n      subtitle: z.string().nullable(),\n      leading: z.string().nullable(),\n      trailing: z.string().nullable(),\n      showChevron: z.boolean().nullable(),\n    }),\n    events: [\"press\"],\n    slots: [],\n    description:\n      \"List row with title, subtitle, and optional leading/trailing text. Bind on.press for the press handler.\",\n    example: {\n      title: \"Settings\",\n      subtitle: \"Manage your preferences\",\n      showChevron: true,\n    },\n  },\n\n  Modal: {\n    props: z.object({\n      visible: z.boolean(),\n      title: z.string().nullable(),\n      animationType: z.enum([\"slide\", \"fade\", \"none\"]).nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Modal overlay dialog. Use $bindState on visible to bind visibility to the state model.\",\n  },\n};\n\n// =============================================================================\n// Standard Action Definitions for React Native\n// =============================================================================\n\n/**\n * Standard action definitions for React Native catalogs.\n *\n * These use React Native core APIs (Alert, Share, Linking) and\n * user-provided functions (navigate).\n */\nexport const standardActionDefinitions = {\n  navigate: {\n    params: z.object({\n      screen: z.string(),\n      params: z.record(z.string(), z.unknown()).nullable(),\n    }),\n    description:\n      \"Navigate to a screen. Calls the user-provided navigation function.\",\n  },\n\n  goBack: {\n    params: z.object({}),\n    description: \"Navigate back to the previous screen.\",\n  },\n\n  showAlert: {\n    params: z.object({\n      title: z.string(),\n      message: z.string().nullable(),\n      buttons: z\n        .array(\n          z.object({\n            text: z.string(),\n            style: z.enum([\"default\", \"cancel\", \"destructive\"]).nullable(),\n            action: z.string().nullable(),\n          }),\n        )\n        .nullable(),\n    }),\n    description: \"Show a native alert dialog using Alert.alert().\",\n  },\n\n  share: {\n    params: z.object({\n      message: z.string(),\n      url: z.string().nullable(),\n      title: z.string().nullable(),\n    }),\n    description: \"Open the native share sheet using Share.share().\",\n  },\n\n  openURL: {\n    params: z.object({\n      url: z.string(),\n    }),\n    description: \"Open a URL in the default browser using Linking.openURL().\",\n  },\n\n  setState: {\n    params: z.object({\n      statePath: z.string(),\n      value: z.unknown(),\n    }),\n    description: \"Update a value in the state model at the given statePath.\",\n  },\n\n  pushState: {\n    params: z.object({\n      statePath: z.string(),\n      value: z.unknown(),\n      clearStatePath: z.string().optional(),\n    }),\n    description:\n      'Append an item to an array in the state model. The value can contain { $state: \"/statePath\" } references to read from current state, and \"$id\" to auto-generate a unique ID. Use clearStatePath to reset another path after pushing (e.g. clear an input field). Example: { statePath: \"/todos\", value: { id: \"$id\", title: { $state: \"/newTodoText\" }, completed: false }, clearStatePath: \"/newTodoText\" }.',\n  },\n\n  removeState: {\n    params: z.object({\n      statePath: z.string(),\n      index: z.number(),\n    }),\n    description:\n      \"Remove an item from an array in the state model at the given index.\",\n  },\n\n  refresh: {\n    params: z.object({\n      target: z.string().nullable(),\n    }),\n    description:\n      \"Trigger a data refresh. Optionally specify a target to refresh.\",\n  },\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Type for a component definition\n */\nexport type ComponentDefinition = {\n  props: z.ZodType;\n  slots: string[];\n  events?: string[];\n  description: string;\n};\n\n/**\n * Type for an action definition\n */\nexport type ActionDefinition = {\n  params: z.ZodType;\n  description: string;\n};\n"
  },
  {
    "path": "packages/react-native/src/components/standard.tsx",
    "content": "import React from \"react\";\nimport {\n  View,\n  Text,\n  Image as RNImage,\n  ScrollView,\n  SafeAreaView,\n  Pressable,\n  TextInput as RNTextInput,\n  Switch as RNSwitch,\n  ActivityIndicator,\n  Modal as RNModal,\n  StyleSheet,\n  Alert,\n  Share,\n  Linking,\n} from \"react-native\";\nimport type { ComponentRenderProps } from \"../renderer\";\nimport type { ComponentRegistry } from \"../renderer\";\nimport { useBoundProp } from \"../hooks\";\n\n// =============================================================================\n// Layout Components\n// =============================================================================\n\nfunction ContainerComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    padding?: number;\n    paddingHorizontal?: number;\n    paddingVertical?: number;\n    margin?: number;\n    backgroundColor?: string;\n    borderRadius?: number;\n    flex?: number;\n  };\n\n  return (\n    <View\n      style={{\n        padding: p.padding ?? undefined,\n        paddingHorizontal: p.paddingHorizontal ?? undefined,\n        paddingVertical: p.paddingVertical ?? undefined,\n        margin: p.margin ?? undefined,\n        backgroundColor: p.backgroundColor ?? undefined,\n        borderRadius: p.borderRadius ?? undefined,\n        flex: p.flex ?? undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\nfunction RowComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    gap?: number;\n    alignItems?: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | \"baseline\";\n    justifyContent?:\n      | \"flex-start\"\n      | \"center\"\n      | \"flex-end\"\n      | \"space-between\"\n      | \"space-around\"\n      | \"space-evenly\";\n    flexWrap?: \"wrap\" | \"nowrap\";\n    padding?: number;\n    flex?: number;\n  };\n\n  return (\n    <View\n      style={{\n        flexDirection: \"row\",\n        gap: p.gap ?? undefined,\n        alignItems: p.alignItems ?? undefined,\n        justifyContent: p.justifyContent ?? undefined,\n        flexWrap: p.flexWrap ?? undefined,\n        padding: p.padding ?? undefined,\n        flex: p.flex ?? undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\nfunction ColumnComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    gap?: number;\n    alignItems?: \"flex-start\" | \"center\" | \"flex-end\" | \"stretch\" | \"baseline\";\n    justifyContent?:\n      | \"flex-start\"\n      | \"center\"\n      | \"flex-end\"\n      | \"space-between\"\n      | \"space-around\"\n      | \"space-evenly\";\n    padding?: number;\n    flex?: number;\n  };\n\n  return (\n    <View\n      style={{\n        flexDirection: \"column\",\n        gap: p.gap ?? undefined,\n        alignItems: p.alignItems ?? undefined,\n        justifyContent: p.justifyContent ?? undefined,\n        padding: p.padding ?? undefined,\n        flex: p.flex ?? undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\nfunction ScrollContainerComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    horizontal?: boolean;\n    showsScrollIndicator?: boolean;\n    padding?: number;\n    backgroundColor?: string;\n    flex?: number;\n  };\n\n  return (\n    <ScrollView\n      horizontal={p.horizontal ?? false}\n      showsVerticalScrollIndicator={p.showsScrollIndicator ?? true}\n      showsHorizontalScrollIndicator={p.showsScrollIndicator ?? true}\n      contentContainerStyle={{\n        padding: p.padding ?? undefined,\n      }}\n      style={{\n        flex: p.flex ?? 1,\n        backgroundColor: p.backgroundColor ?? undefined,\n      }}\n    >\n      {children}\n    </ScrollView>\n  );\n}\n\nfunction SafeAreaComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    backgroundColor?: string;\n  };\n\n  return (\n    <SafeAreaView\n      style={{\n        flex: 1,\n        backgroundColor: p.backgroundColor ?? undefined,\n      }}\n    >\n      {children}\n    </SafeAreaView>\n  );\n}\n\nfunction SpacerComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    size?: number;\n    flex?: number;\n  };\n\n  return (\n    <View\n      style={{\n        width: p.size ?? undefined,\n        height: p.size ?? undefined,\n        flex: p.flex ?? undefined,\n      }}\n    />\n  );\n}\n\nfunction PressableComponent({ children, emit }: ComponentRenderProps) {\n  return (\n    <Pressable\n      onPress={() => emit(\"press\")}\n      onLongPress={() => emit(\"longPress\")}\n      style={({ pressed }) => ({\n        opacity: pressed ? 0.7 : 1,\n      })}\n    >\n      {children}\n    </Pressable>\n  );\n}\n\nfunction DividerComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    direction?: \"horizontal\" | \"vertical\";\n    color?: string;\n    thickness?: number;\n    margin?: number;\n  };\n\n  const isVertical = p.direction === \"vertical\";\n  const thickness = p.thickness ?? 1;\n  const color = p.color ?? \"#e5e7eb\";\n\n  return (\n    <View\n      style={{\n        width: isVertical ? thickness : \"100%\",\n        height: isVertical ? \"100%\" : thickness,\n        backgroundColor: color,\n        marginVertical: isVertical ? 0 : (p.margin ?? 8),\n        marginHorizontal: isVertical ? (p.margin ?? 8) : 0,\n      }}\n    />\n  );\n}\n\n// =============================================================================\n// Content Components\n// =============================================================================\n\nconst headingSizes = {\n  h1: 32,\n  h2: 24,\n  h3: 20,\n  h4: 16,\n};\n\nfunction HeadingComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    text: string;\n    level?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n    color?: string;\n    align?: \"left\" | \"center\" | \"right\";\n  };\n\n  const level = p.level ?? \"h2\";\n\n  return (\n    <Text\n      style={{\n        fontSize: headingSizes[level],\n        fontWeight: \"700\",\n        color: p.color ?? \"#111827\",\n        textAlign: p.align ?? \"left\",\n      }}\n    >\n      {p.text}\n    </Text>\n  );\n}\n\nfunction ParagraphComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    text: string;\n    color?: string;\n    align?: \"left\" | \"center\" | \"right\";\n    numberOfLines?: number;\n    fontSize?: number;\n  };\n\n  return (\n    <Text\n      numberOfLines={p.numberOfLines ?? undefined}\n      style={{\n        fontSize: p.fontSize ?? 16,\n        lineHeight: (p.fontSize ?? 16) * 1.5,\n        color: p.color ?? \"#374151\",\n        textAlign: p.align ?? \"left\",\n      }}\n    >\n      {p.text}\n    </Text>\n  );\n}\n\nconst labelSizes = {\n  xs: 10,\n  sm: 12,\n  md: 14,\n};\n\nfunction LabelComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    text: string;\n    color?: string;\n    bold?: boolean;\n    size?: \"xs\" | \"sm\" | \"md\";\n  };\n\n  return (\n    <Text\n      style={{\n        fontSize: labelSizes[p.size ?? \"sm\"],\n        fontWeight: p.bold ? \"600\" : \"400\",\n        color: p.color ?? \"#6b7280\",\n      }}\n    >\n      {p.text}\n    </Text>\n  );\n}\n\nfunction ImageComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    src: string;\n    alt?: string;\n    width?: number;\n    height?: number;\n    resizeMode?: \"cover\" | \"contain\" | \"stretch\" | \"center\";\n    borderRadius?: number;\n  };\n\n  return (\n    <RNImage\n      source={{ uri: p.src }}\n      accessibilityLabel={p.alt ?? undefined}\n      resizeMode={p.resizeMode ?? \"cover\"}\n      style={{\n        width: p.width ?? \"100%\",\n        height: p.height ?? 200,\n        borderRadius: p.borderRadius ?? 0,\n      }}\n    />\n  );\n}\n\nconst avatarSizeMap = {\n  sm: 32,\n  md: 48,\n  lg: 64,\n  xl: 96,\n};\n\nfunction AvatarComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    src?: string;\n    initials?: string;\n    size?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n    backgroundColor?: string;\n  };\n\n  const size = avatarSizeMap[p.size ?? \"md\"];\n  const bgColor = p.backgroundColor ?? \"#6366f1\";\n\n  if (p.src) {\n    return (\n      <RNImage\n        source={{ uri: p.src }}\n        style={{\n          width: size,\n          height: size,\n          borderRadius: size / 2,\n          backgroundColor: \"#e5e7eb\",\n        }}\n      />\n    );\n  }\n\n  return (\n    <View\n      style={{\n        width: size,\n        height: size,\n        borderRadius: size / 2,\n        backgroundColor: bgColor,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <Text\n        style={{\n          color: \"#ffffff\",\n          fontSize: size * 0.4,\n          fontWeight: \"600\",\n        }}\n      >\n        {p.initials ?? \"?\"}\n      </Text>\n    </View>\n  );\n}\n\nconst badgeVariantColors: Record<string, { bg: string; text: string }> = {\n  default: { bg: \"#e5e7eb\", text: \"#374151\" },\n  info: { bg: \"#dbeafe\", text: \"#1d4ed8\" },\n  success: { bg: \"#dcfce7\", text: \"#15803d\" },\n  warning: { bg: \"#fef3c7\", text: \"#92400e\" },\n  error: { bg: \"#fee2e2\", text: \"#dc2626\" },\n};\n\nfunction BadgeComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    label: string;\n    variant?: \"default\" | \"info\" | \"success\" | \"warning\" | \"error\";\n  };\n\n  const colors = badgeVariantColors[p.variant ?? \"default\"] ?? {\n    bg: \"#e5e7eb\",\n    text: \"#374151\",\n  };\n\n  return (\n    <View\n      style={{\n        backgroundColor: colors.bg,\n        paddingHorizontal: 8,\n        paddingVertical: 2,\n        borderRadius: 9999,\n        alignSelf: \"flex-start\",\n      }}\n    >\n      <Text\n        style={{\n          fontSize: 12,\n          fontWeight: \"600\",\n          color: colors.text,\n        }}\n      >\n        {p.label}\n      </Text>\n    </View>\n  );\n}\n\nfunction ChipComponent({ element, emit }: ComponentRenderProps) {\n  const p = element.props as {\n    label: string;\n    selected?: boolean;\n    backgroundColor?: string;\n  };\n\n  const bgColor = p.selected ? \"#6366f1\" : (p.backgroundColor ?? \"#f3f4f6\");\n  const textColor = p.selected ? \"#ffffff\" : \"#374151\";\n  const hasRemove = !!element.on?.remove;\n\n  return (\n    <Pressable\n      onPress={() => emit(\"press\")}\n      style={{\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        backgroundColor: bgColor,\n        paddingHorizontal: 12,\n        paddingVertical: 6,\n        borderRadius: 9999,\n        alignSelf: \"flex-start\",\n      }}\n    >\n      <Text style={{ fontSize: 14, color: textColor }}>{p.label}</Text>\n      {hasRemove && (\n        <Pressable onPress={() => emit(\"remove\")} style={{ marginLeft: 4 }}>\n          <Text style={{ fontSize: 14, color: textColor, fontWeight: \"700\" }}>\n            x\n          </Text>\n        </Pressable>\n      )}\n    </Pressable>\n  );\n}\n\n// =============================================================================\n// Input Components\n// =============================================================================\n\nconst buttonVariantStyles: Record<\n  string,\n  { bg: string; text: string; border?: string }\n> = {\n  primary: { bg: \"#3b82f6\", text: \"#ffffff\" },\n  secondary: { bg: \"#6b7280\", text: \"#ffffff\" },\n  danger: { bg: \"#dc2626\", text: \"#ffffff\" },\n  outline: { bg: \"transparent\", text: \"#3b82f6\", border: \"#3b82f6\" },\n  ghost: { bg: \"transparent\", text: \"#374151\" },\n};\n\nconst buttonSizeStyles: Record<\n  string,\n  { paddingH: number; paddingV: number; fontSize: number }\n> = {\n  sm: { paddingH: 12, paddingV: 6, fontSize: 13 },\n  md: { paddingH: 16, paddingV: 10, fontSize: 15 },\n  lg: { paddingH: 24, paddingV: 14, fontSize: 17 },\n};\n\nfunction ButtonComponent({ element, emit }: ComponentRenderProps) {\n  const p = element.props as {\n    label: string;\n    variant?: \"primary\" | \"secondary\" | \"danger\" | \"outline\" | \"ghost\";\n    size?: \"sm\" | \"md\" | \"lg\";\n    disabled?: boolean;\n    loading?: boolean;\n  };\n\n  const variant = buttonVariantStyles[p.variant ?? \"primary\"] ?? {\n    bg: \"#3b82f6\",\n    text: \"#ffffff\",\n  };\n  const size = buttonSizeStyles[p.size ?? \"md\"] ?? {\n    paddingH: 16,\n    paddingV: 10,\n    fontSize: 15,\n  };\n  const disabled = p.disabled || p.loading;\n\n  return (\n    <Pressable\n      disabled={disabled}\n      onPress={() => emit(\"press\")}\n      style={({ pressed }) => ({\n        backgroundColor: variant.bg,\n        paddingHorizontal: size.paddingH,\n        paddingVertical: size.paddingV,\n        borderRadius: 8,\n        alignItems: \"center\" as const,\n        justifyContent: \"center\" as const,\n        flexDirection: \"row\" as const,\n        opacity: disabled ? 0.5 : pressed ? 0.8 : 1,\n        borderWidth: variant.border ? 1 : 0,\n        borderColor: variant.border,\n        alignSelf: \"flex-start\" as const,\n      })}\n    >\n      {p.loading && (\n        <ActivityIndicator\n          size=\"small\"\n          color={variant.text}\n          style={{ marginRight: 8 }}\n        />\n      )}\n      <Text\n        style={{\n          color: variant.text,\n          fontSize: size.fontSize,\n          fontWeight: \"600\",\n        }}\n      >\n        {p.label}\n      </Text>\n    </Pressable>\n  );\n}\n\nfunction TextInputComponent({ element, bindings }: ComponentRenderProps) {\n  const p = element.props as {\n    placeholder?: string;\n    value?: string;\n    secureTextEntry?: boolean;\n    keyboardType?:\n      | \"default\"\n      | \"email-address\"\n      | \"numeric\"\n      | \"phone-pad\"\n      | \"url\";\n    multiline?: boolean;\n    numberOfLines?: number;\n    label?: string;\n    flex?: number;\n  };\n\n  const [boundValue, setBoundValue] = useBoundProp<string>(\n    p.value as string | undefined,\n    bindings?.value,\n  );\n  const displayValue = boundValue ?? \"\";\n\n  return (\n    <View style={p.flex != null ? { flex: p.flex } : undefined}>\n      {p.label && <Text style={inputStyles.label}>{p.label}</Text>}\n      <RNTextInput\n        placeholder={p.placeholder ?? undefined}\n        value={displayValue}\n        onChangeText={bindings?.value ? setBoundValue : undefined}\n        secureTextEntry={p.secureTextEntry ?? false}\n        keyboardType={p.keyboardType ?? \"default\"}\n        multiline={p.multiline ?? false}\n        numberOfLines={p.numberOfLines ?? undefined}\n        style={[\n          inputStyles.input,\n          p.multiline && {\n            minHeight: (p.numberOfLines ?? 3) * 20,\n            textAlignVertical: \"top\" as const,\n          },\n        ]}\n      />\n    </View>\n  );\n}\n\nfunction SwitchComponent({ element, bindings }: ComponentRenderProps) {\n  const p = element.props as {\n    checked?: boolean;\n    label?: string;\n    disabled?: boolean;\n  };\n\n  const [checked, setChecked] = useBoundProp<boolean>(\n    p.checked as boolean | undefined,\n    bindings?.checked,\n  );\n  const displayValue = checked ?? false;\n\n  return (\n    <View style={{ flexDirection: \"row\", alignItems: \"center\", gap: 8 }}>\n      <RNSwitch\n        value={displayValue}\n        onValueChange={bindings?.checked ? setChecked : undefined}\n        disabled={p.disabled ?? false}\n      />\n      {p.label && (\n        <Text style={{ fontSize: 16, color: \"#374151\" }}>{p.label}</Text>\n      )}\n    </View>\n  );\n}\n\nfunction CheckboxComponent({ element, bindings }: ComponentRenderProps) {\n  const p = element.props as {\n    checked?: boolean;\n    label?: string;\n    disabled?: boolean;\n  };\n\n  const [checked, setChecked] = useBoundProp<boolean>(\n    p.checked as boolean | undefined,\n    bindings?.checked,\n  );\n  const isChecked = checked ?? false;\n\n  return (\n    <Pressable\n      disabled={p.disabled ?? false}\n      onPress={() => {\n        if (bindings?.checked) {\n          setChecked(!isChecked);\n        }\n      }}\n      style={{\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        gap: 8,\n        opacity: p.disabled ? 0.5 : 1,\n      }}\n    >\n      <View\n        style={{\n          width: 20,\n          height: 20,\n          borderRadius: 4,\n          borderWidth: 2,\n          borderColor: isChecked ? \"#3b82f6\" : \"#d1d5db\",\n          backgroundColor: isChecked ? \"#3b82f6\" : \"transparent\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        {isChecked && (\n          <Text style={{ color: \"#ffffff\", fontSize: 12, fontWeight: \"700\" }}>\n            {\"✓\"}\n          </Text>\n        )}\n      </View>\n      {p.label && (\n        <Text style={{ fontSize: 16, color: \"#374151\" }}>{p.label}</Text>\n      )}\n    </Pressable>\n  );\n}\n\nfunction SliderComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    min?: number;\n    max?: number;\n    step?: number;\n    value?: number;\n    label?: string;\n    color?: string;\n  };\n\n  const min = p.min ?? 0;\n  const max = p.max ?? 100;\n  const value = p.value ?? min;\n  const progress = max > min ? (value - min) / (max - min) : 0;\n  const trackColor = p.color ?? \"#3b82f6\";\n\n  return (\n    <View>\n      {p.label && (\n        <View\n          style={{\n            flexDirection: \"row\",\n            justifyContent: \"space-between\",\n            marginBottom: 4,\n          }}\n        >\n          <Text style={{ fontSize: 14, color: \"#374151\" }}>{p.label}</Text>\n          <Text style={{ fontSize: 14, color: \"#6b7280\" }}>{value}</Text>\n        </View>\n      )}\n      <View\n        style={{\n          height: 6,\n          backgroundColor: \"#e5e7eb\",\n          borderRadius: 3,\n          overflow: \"hidden\",\n        }}\n      >\n        <View\n          style={{\n            width: `${Math.min(Math.max(progress * 100, 0), 100)}%`,\n            height: \"100%\",\n            backgroundColor: trackColor,\n            borderRadius: 3,\n          }}\n        />\n      </View>\n    </View>\n  );\n}\n\nfunction SearchBarComponent({ element, emit }: ComponentRenderProps) {\n  const p = element.props as {\n    placeholder?: string;\n    value?: string;\n  };\n\n  return (\n    <View\n      style={{\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        backgroundColor: \"#f3f4f6\",\n        borderRadius: 8,\n        paddingHorizontal: 12,\n      }}\n    >\n      <Text style={{ fontSize: 16, color: \"#9ca3af\", marginRight: 8 }}>\n        {\"Search\"}\n      </Text>\n      <RNTextInput\n        placeholder={p.placeholder ?? \"Search...\"}\n        value={p.value ?? undefined}\n        returnKeyType=\"search\"\n        onSubmitEditing={() => emit(\"submit\")}\n        style={{\n          flex: 1,\n          paddingVertical: 10,\n          fontSize: 16,\n          color: \"#111827\",\n        }}\n      />\n    </View>\n  );\n}\n\n// =============================================================================\n// Feedback Components\n// =============================================================================\n\nfunction SpinnerComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    size?: \"small\" | \"large\";\n    color?: string;\n  };\n\n  return (\n    <ActivityIndicator size={p.size ?? \"small\"} color={p.color ?? \"#3b82f6\"} />\n  );\n}\n\nfunction ProgressBarComponent({ element }: ComponentRenderProps) {\n  const p = element.props as {\n    progress: number;\n    color?: string;\n    trackColor?: string;\n    height?: number;\n  };\n\n  const height = p.height ?? 6;\n  const progress = Math.min(Math.max(p.progress, 0), 1);\n\n  return (\n    <View\n      style={{\n        height,\n        backgroundColor: p.trackColor ?? \"#e5e7eb\",\n        borderRadius: height / 2,\n        overflow: \"hidden\",\n      }}\n    >\n      <View\n        style={{\n          width: `${progress * 100}%`,\n          height: \"100%\",\n          backgroundColor: p.color ?? \"#3b82f6\",\n          borderRadius: height / 2,\n        }}\n      />\n    </View>\n  );\n}\n\n// =============================================================================\n// Composite Components\n// =============================================================================\n\nfunction CardComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    title?: string;\n    subtitle?: string;\n    padding?: number;\n    backgroundColor?: string;\n    borderRadius?: number;\n    elevated?: boolean;\n  };\n\n  const elevated = p.elevated !== false;\n  const padding = p.padding ?? 16;\n\n  return (\n    <View\n      style={[\n        {\n          backgroundColor: p.backgroundColor ?? \"#ffffff\",\n          borderRadius: p.borderRadius ?? 12,\n          padding,\n        },\n        elevated && {\n          shadowColor: \"#000\",\n          shadowOffset: { width: 0, height: 2 },\n          shadowOpacity: 0.1,\n          shadowRadius: 8,\n          elevation: 3,\n        },\n      ]}\n    >\n      {p.title && (\n        <Text\n          style={{\n            fontSize: 18,\n            fontWeight: \"600\",\n            color: \"#111827\",\n            marginBottom: p.subtitle ? 2 : 12,\n          }}\n        >\n          {p.title}\n        </Text>\n      )}\n      {p.subtitle && (\n        <Text\n          style={{\n            fontSize: 14,\n            color: \"#6b7280\",\n            marginBottom: 12,\n          }}\n        >\n          {p.subtitle}\n        </Text>\n      )}\n      {children}\n    </View>\n  );\n}\n\nfunction ListItemComponent({ element, emit }: ComponentRenderProps) {\n  const p = element.props as {\n    title: string;\n    subtitle?: string;\n    leading?: string;\n    trailing?: string;\n    showChevron?: boolean;\n  };\n\n  const hasPress = !!element.on?.press;\n\n  const content = (\n    <View\n      style={{\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        paddingVertical: 12,\n        paddingHorizontal: 16,\n      }}\n    >\n      {p.leading && (\n        <Text\n          style={{\n            fontSize: 16,\n            color: \"#6b7280\",\n            marginRight: 12,\n            width: 24,\n            textAlign: \"center\",\n          }}\n        >\n          {p.leading}\n        </Text>\n      )}\n      <View style={{ flex: 1 }}>\n        <Text style={{ fontSize: 16, color: \"#111827\" }}>{p.title}</Text>\n        {p.subtitle && (\n          <Text style={{ fontSize: 14, color: \"#6b7280\", marginTop: 2 }}>\n            {p.subtitle}\n          </Text>\n        )}\n      </View>\n      {p.trailing && (\n        <Text style={{ fontSize: 14, color: \"#6b7280\", marginLeft: 8 }}>\n          {p.trailing}\n        </Text>\n      )}\n      {p.showChevron && (\n        <Text style={{ fontSize: 16, color: \"#9ca3af\", marginLeft: 8 }}>\n          {\">\"}\n        </Text>\n      )}\n    </View>\n  );\n\n  if (hasPress) {\n    return (\n      <Pressable\n        onPress={() => emit(\"press\")}\n        style={({ pressed }) => ({\n          opacity: pressed ? 0.7 : 1,\n          backgroundColor: pressed ? \"#f9fafb\" : \"transparent\",\n        })}\n      >\n        {content}\n      </Pressable>\n    );\n  }\n\n  return content;\n}\n\nfunction ModalComponent({ element, children }: ComponentRenderProps) {\n  const p = element.props as {\n    visible: boolean;\n    title?: string;\n    animationType?: \"slide\" | \"fade\" | \"none\";\n  };\n\n  return (\n    <RNModal\n      visible={p.visible}\n      animationType={p.animationType ?? \"slide\"}\n      transparent\n    >\n      <View style={modalStyles.overlay}>\n        <View style={modalStyles.content}>\n          {p.title && <Text style={modalStyles.title}>{p.title}</Text>}\n          {children}\n        </View>\n      </View>\n    </RNModal>\n  );\n}\n\n// =============================================================================\n// Standard Action Handlers\n// =============================================================================\n\n/**\n * Create standard action handlers for React Native.\n *\n * @param options - Configuration for standard actions\n * @returns Action handler map compatible with ActionProvider\n */\nexport function createStandardActionHandlers(options?: {\n  /** Navigation function - called by navigate and goBack actions */\n  navigate?: (screen: string, params?: Record<string, unknown>) => void;\n  /** Go back function */\n  goBack?: () => void;\n  /** Called when setData action is triggered */\n  onSetData?: (path: string, value: unknown) => void;\n  /** Called when refresh action is triggered */\n  onRefresh?: (target?: string) => void;\n}): Record<string, (params: Record<string, unknown>) => Promise<void>> {\n  return {\n    navigate: async (params) => {\n      const screen = params.screen as string;\n      const navParams = params.params as Record<string, unknown> | undefined;\n      options?.navigate?.(screen, navParams ?? undefined);\n    },\n\n    goBack: async () => {\n      options?.goBack?.();\n    },\n\n    showAlert: async (params) => {\n      const title = params.title as string;\n      const message = (params.message as string) ?? undefined;\n      const buttons = params.buttons as\n        | Array<{\n            text: string;\n            style?: \"default\" | \"cancel\" | \"destructive\";\n            action?: string;\n          }>\n        | undefined;\n\n      const alertButtons = buttons?.map((btn) => ({\n        text: btn.text,\n        style: btn.style as \"default\" | \"cancel\" | \"destructive\" | undefined,\n      }));\n\n      Alert.alert(title, message, alertButtons);\n    },\n\n    share: async (params) => {\n      const message = params.message as string;\n      const url = (params.url as string) ?? undefined;\n      const title = (params.title as string) ?? undefined;\n\n      await Share.share({ message, url, title });\n    },\n\n    openURL: async (params) => {\n      const url = params.url as string;\n      const canOpen = await Linking.canOpenURL(url);\n      if (canOpen) {\n        await Linking.openURL(url);\n      }\n    },\n\n    setData: async (params) => {\n      const statePath = params.statePath as string;\n      const value = params.value;\n      options?.onSetData?.(statePath, value);\n    },\n\n    refresh: async (params) => {\n      const target = (params.target as string) ?? undefined;\n      options?.onRefresh?.(target);\n    },\n  };\n}\n\n// =============================================================================\n// Standard Components Registry\n// =============================================================================\n\n/**\n * Standard component registry with all built-in React Native components.\n * Pass to Renderer or merge with custom components.\n */\nexport const standardComponents: ComponentRegistry = {\n  // Layout\n  Container: ContainerComponent,\n  Row: RowComponent,\n  Column: ColumnComponent,\n  ScrollContainer: ScrollContainerComponent,\n  SafeArea: SafeAreaComponent,\n  Pressable: PressableComponent,\n  Spacer: SpacerComponent,\n  Divider: DividerComponent,\n  // Content\n  Heading: HeadingComponent,\n  Paragraph: ParagraphComponent,\n  Label: LabelComponent,\n  Image: ImageComponent,\n  Avatar: AvatarComponent,\n  Badge: BadgeComponent,\n  Chip: ChipComponent,\n  // Input\n  Button: ButtonComponent,\n  TextInput: TextInputComponent,\n  Switch: SwitchComponent,\n  Checkbox: CheckboxComponent,\n  Slider: SliderComponent,\n  SearchBar: SearchBarComponent,\n  // Feedback\n  Spinner: SpinnerComponent,\n  ProgressBar: ProgressBarComponent,\n  // Composite\n  Card: CardComponent,\n  ListItem: ListItemComponent,\n  Modal: ModalComponent,\n};\n\n// =============================================================================\n// Shared Styles\n// =============================================================================\n\nconst inputStyles = StyleSheet.create({\n  label: {\n    fontSize: 14,\n    fontWeight: \"500\",\n    color: \"#374151\",\n    marginBottom: 4,\n  },\n  input: {\n    borderWidth: 1,\n    borderColor: \"#d1d5db\",\n    borderRadius: 8,\n    paddingHorizontal: 12,\n    paddingVertical: 10,\n    fontSize: 16,\n    color: \"#111827\",\n    backgroundColor: \"#ffffff\",\n  },\n});\n\nconst modalStyles = StyleSheet.create({\n  overlay: {\n    flex: 1,\n    backgroundColor: \"rgba(0, 0, 0, 0.5)\",\n    justifyContent: \"flex-end\",\n  },\n  content: {\n    backgroundColor: \"#ffffff\",\n    borderTopLeftRadius: 16,\n    borderTopRightRadius: 16,\n    padding: 24,\n    maxHeight: \"80%\",\n  },\n  title: {\n    fontSize: 20,\n    fontWeight: \"600\",\n    color: \"#111827\",\n    marginBottom: 16,\n  },\n});\n"
  },
  {
    "path": "packages/react-native/src/contexts/actions.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport { Modal, View, Text, Pressable, StyleSheet } from \"react-native\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Generate a unique ID for use with the \"$id\" token.\n * Combines a timestamp with a random suffix for uniqueness.\n */\nlet idCounter = 0;\nfunction generateUniqueId(): string {\n  idCounter += 1;\n  return `${Date.now()}-${idCounter}`;\n}\n\n/**\n * Deep-resolve dynamic value references within an object.\n *\n * Supported tokens:\n * - `{ $state: \"/statePath\" }` - read a value from state\n * - `\"$id\"` (string) or `{ \"$id\": true }` - generate a unique ID\n *\n * This allows pushState values to contain references to current state\n * and auto-generated IDs.\n */\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  // \"$id\" string token -> generate unique ID\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    // { $state: \"/foo\" } -> read from state\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    // { \"$id\": true } -> generate unique ID (single-key object)\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  // Recurse into arrays\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  // Recurse into plain objects\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\n/**\n * Pending confirmation state\n */\nexport interface PendingConfirmation {\n  /** The resolved action */\n  action: ResolvedAction;\n  /** The action handler */\n  handler: ActionHandler;\n  /** Resolve callback */\n  resolve: () => void;\n  /** Reject callback */\n  reject: () => void;\n}\n\n/**\n * Action context value\n */\nexport interface ActionContextValue {\n  /** Registered action handlers */\n  handlers: Record<string, ActionHandler>;\n  /** Currently loading action names */\n  loadingActions: Set<string>;\n  /** Pending confirmation dialog */\n  pendingConfirmation: PendingConfirmation | null;\n  /** Execute an action binding */\n  execute: (binding: ActionBinding) => Promise<void>;\n  /** Confirm the pending action */\n  confirm: () => void;\n  /** Cancel the pending action */\n  cancel: () => void;\n  /** Register an action handler */\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ActionContext = createContext<ActionContextValue | null>(null);\n\n/**\n * Props for ActionProvider\n */\nexport interface ActionProviderProps {\n  /** Initial action handlers */\n  handlers?: Record<string, ActionHandler>;\n  /** Navigation function */\n  navigate?: (path: string) => void;\n  children: ReactNode;\n}\n\n/**\n * Provider for action execution\n */\nexport function ActionProvider({\n  handlers: initialHandlers = {},\n  navigate,\n  children,\n}: ActionProviderProps) {\n  const { get, set, getSnapshot } = useStateStore();\n  const [handlers, setHandlers] =\n    useState<Record<string, ActionHandler>>(initialHandlers);\n  const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());\n  const [pendingConfirmation, setPendingConfirmation] =\n    useState<PendingConfirmation | null>(null);\n\n  const registerHandler = useCallback(\n    (name: string, handler: ActionHandler) => {\n      setHandlers((prev) => ({ ...prev, [name]: handler }));\n    },\n    [],\n  );\n\n  const execute = useCallback(\n    async (binding: ActionBinding) => {\n      const resolved = resolveAction(binding, getSnapshot());\n\n      // Built-in: setState updates the StateProvider state directly\n      if (resolved.action === \"setState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const value = resolved.params.value;\n        if (statePath) {\n          set(statePath, value);\n        }\n        return;\n      }\n\n      // Built-in: pushState appends an item to an array in state.\n      // Supports dynamic values inside the value object via { $state: \"/...\" } syntax.\n      if (resolved.action === \"pushState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const rawValue = resolved.params.value;\n        if (statePath) {\n          const resolvedValue = deepResolveValue(rawValue, get);\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(statePath, [...arr, resolvedValue]);\n          // Optionally clear a state path after pushing (e.g. clear the input)\n          const clearStatePath = resolved.params.clearStatePath as\n            | string\n            | undefined;\n          if (clearStatePath) {\n            set(clearStatePath, \"\");\n          }\n        }\n        return;\n      }\n\n      // Built-in: removeState removes an item from an array in state by index.\n      if (resolved.action === \"removeState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const index = resolved.params.index as number;\n        if (statePath !== undefined && index !== undefined) {\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(\n            statePath,\n            arr.filter((_, i) => i !== index),\n          );\n        }\n        return;\n      }\n\n      // Built-in: push navigates to a new screen by updating state.\n      // Pushes the current screen onto /navStack and sets /currentScreen.\n      if (resolved.action === \"push\" && resolved.params) {\n        const screen = resolved.params.screen as string;\n        if (screen) {\n          const currentScreen = get(\"/currentScreen\") as string | undefined;\n          const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n          if (currentScreen) {\n            set(\"/navStack\", [...navStack, currentScreen]);\n          } else {\n            // No current screen set yet -- push a sentinel so pop returns here\n            set(\"/navStack\", [...navStack, \"\"]);\n          }\n          set(\"/currentScreen\", screen);\n        }\n        return;\n      }\n\n      // Built-in: pop navigates back to the previous screen.\n      // Pops the last entry from /navStack and restores /currentScreen.\n      if (resolved.action === \"pop\") {\n        const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n        if (navStack.length > 0) {\n          const previousScreen = navStack[navStack.length - 1];\n          set(\"/navStack\", navStack.slice(0, -1));\n          if (previousScreen) {\n            set(\"/currentScreen\", previousScreen);\n          } else {\n            // Sentinel empty string = clear currentScreen (return to default)\n            set(\"/currentScreen\", undefined);\n          }\n        }\n        return;\n      }\n\n      const handler = handlers[resolved.action];\n\n      if (!handler) {\n        console.warn(`No handler registered for action: ${resolved.action}`);\n        return;\n      }\n\n      // If confirmation is required, show dialog\n      if (resolved.confirm) {\n        return new Promise<void>((resolve, reject) => {\n          setPendingConfirmation({\n            action: resolved,\n            handler,\n            resolve: () => {\n              setPendingConfirmation(null);\n              resolve();\n            },\n            reject: () => {\n              setPendingConfirmation(null);\n              reject(new Error(\"Action cancelled\"));\n            },\n          });\n        }).then(async () => {\n          setLoadingActions((prev) => new Set(prev).add(resolved.action));\n          try {\n            await executeAction({\n              action: resolved,\n              handler,\n              setState: set,\n              navigate,\n              executeAction: async (name) => {\n                const subBinding: ActionBinding = { action: name };\n                await execute(subBinding);\n              },\n            });\n          } finally {\n            setLoadingActions((prev) => {\n              const next = new Set(prev);\n              next.delete(resolved.action);\n              return next;\n            });\n          }\n        });\n      }\n\n      // Execute immediately\n      setLoadingActions((prev) => new Set(prev).add(resolved.action));\n      try {\n        await executeAction({\n          action: resolved,\n          handler,\n          setState: set,\n          navigate,\n          executeAction: async (name) => {\n            const subBinding: ActionBinding = { action: name };\n            await execute(subBinding);\n          },\n        });\n      } finally {\n        setLoadingActions((prev) => {\n          const next = new Set(prev);\n          next.delete(resolved.action);\n          return next;\n        });\n      }\n    },\n    [handlers, get, set, getSnapshot, navigate],\n  );\n\n  const confirm = useCallback(() => {\n    pendingConfirmation?.resolve();\n  }, [pendingConfirmation]);\n\n  const cancel = useCallback(() => {\n    pendingConfirmation?.reject();\n  }, [pendingConfirmation]);\n\n  const value = useMemo<ActionContextValue>(\n    () => ({\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    }),\n    [\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    ],\n  );\n\n  return (\n    <ActionContext.Provider value={value}>{children}</ActionContext.Provider>\n  );\n}\n\n/**\n * Hook to access action context\n */\nexport function useActions(): ActionContextValue {\n  const ctx = useContext(ActionContext);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to execute an action binding\n */\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: boolean;\n} {\n  const { execute, loadingActions } = useActions();\n  const isLoading = loadingActions.has(binding.action);\n\n  const executeAction = useCallback(() => execute(binding), [execute, binding]);\n\n  return { execute: executeAction, isLoading };\n}\n\n/**\n * Props for ConfirmDialog component\n */\nexport interface ConfirmDialogProps {\n  /** The confirmation config */\n  confirm: ActionConfirm;\n  /** Called when confirmed */\n  onConfirm: () => void;\n  /** Called when cancelled */\n  onCancel: () => void;\n}\n\n/**\n * Default confirmation dialog component for React Native\n */\nexport function ConfirmDialog({\n  confirm,\n  onConfirm,\n  onCancel,\n}: ConfirmDialogProps) {\n  const isDanger = confirm.variant === \"danger\";\n\n  return (\n    <Modal transparent animationType=\"fade\" visible onRequestClose={onCancel}>\n      <Pressable style={styles.overlay} onPress={onCancel}>\n        <Pressable style={styles.dialog} onPress={() => {}}>\n          <Text style={styles.title}>{confirm.title}</Text>\n          <Text style={styles.message}>{confirm.message}</Text>\n          <View style={styles.buttons}>\n            <Pressable style={styles.cancelButton} onPress={onCancel}>\n              <Text style={styles.cancelButtonText}>\n                {confirm.cancelLabel ?? \"Cancel\"}\n              </Text>\n            </Pressable>\n            <Pressable\n              style={[\n                styles.confirmButton,\n                isDanger && styles.confirmButtonDanger,\n              ]}\n              onPress={onConfirm}\n            >\n              <Text style={styles.confirmButtonText}>\n                {confirm.confirmLabel ?? \"Confirm\"}\n              </Text>\n            </Pressable>\n          </View>\n        </Pressable>\n      </Pressable>\n    </Modal>\n  );\n}\n\nconst styles = StyleSheet.create({\n  overlay: {\n    flex: 1,\n    backgroundColor: \"rgba(0, 0, 0, 0.5)\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  dialog: {\n    backgroundColor: \"white\",\n    borderRadius: 8,\n    padding: 24,\n    maxWidth: 400,\n    width: \"90%\",\n    shadowColor: \"#000\",\n    shadowOffset: { width: 0, height: 20 },\n    shadowOpacity: 0.1,\n    shadowRadius: 25,\n    elevation: 10,\n  },\n  title: {\n    fontSize: 18,\n    fontWeight: \"600\",\n    marginBottom: 8,\n  },\n  message: {\n    fontSize: 14,\n    color: \"#6b7280\",\n    marginBottom: 24,\n  },\n  buttons: {\n    flexDirection: \"row\",\n    justifyContent: \"flex-end\",\n    gap: 12,\n  },\n  cancelButton: {\n    paddingVertical: 8,\n    paddingHorizontal: 16,\n    borderRadius: 6,\n    borderWidth: 1,\n    borderColor: \"#d1d5db\",\n    backgroundColor: \"white\",\n  },\n  cancelButtonText: {\n    fontSize: 14,\n    color: \"#374151\",\n  },\n  confirmButton: {\n    paddingVertical: 8,\n    paddingHorizontal: 16,\n    borderRadius: 6,\n    backgroundColor: \"#3b82f6\",\n  },\n  confirmButtonDanger: {\n    backgroundColor: \"#dc2626\",\n  },\n  confirmButtonText: {\n    fontSize: 14,\n    color: \"white\",\n  },\n});\n"
  },
  {
    "path": "packages/react-native/src/contexts/repeat-scope.tsx",
    "content": "import React, { createContext, useContext, type ReactNode } from \"react\";\n\n/**\n * Repeat scope value provided to child elements inside a repeated element.\n */\nexport interface RepeatScopeValue {\n  /** The current array item object */\n  item: unknown;\n  /** Index of the current item in the array */\n  index: number;\n  /** Absolute state path to the current array item (e.g. \"/todos/0\") — used for statePath two-way binding */\n  basePath: string;\n}\n\nconst RepeatScopeContext = createContext<RepeatScopeValue | null>(null);\n\n/**\n * Provides repeat scope to child elements so $item and $index expressions resolve correctly.\n */\nexport function RepeatScopeProvider({\n  item,\n  index,\n  basePath,\n  children,\n}: RepeatScopeValue & { children: ReactNode }) {\n  return (\n    <RepeatScopeContext.Provider value={{ item, index, basePath }}>\n      {children}\n    </RepeatScopeContext.Provider>\n  );\n}\n\n/**\n * Read the current repeat scope (or null if not inside a repeated element).\n */\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return useContext(RepeatScopeContext);\n}\n"
  },
  {
    "path": "packages/react-native/src/contexts/state.tsx",
    "content": "export {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"@internal/react-state\";\n"
  },
  {
    "path": "packages/react-native/src/contexts/validation.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Field validation state\n */\nexport interface FieldValidationState {\n  /** Whether the field has been touched */\n  touched: boolean;\n  /** Whether the field has been validated */\n  validated: boolean;\n  /** Validation result */\n  result: ValidationResult | null;\n}\n\n/**\n * Validation context value\n */\nexport interface ValidationContextValue {\n  /** Custom validation functions from catalog */\n  customFunctions: Record<string, ValidationFunction>;\n  /** Validation state by field path */\n  fieldStates: Record<string, FieldValidationState>;\n  /** Validate a field */\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  /** Mark field as touched */\n  touch: (path: string) => void;\n  /** Clear validation for a field */\n  clear: (path: string) => void;\n  /** Validate all fields */\n  validateAll: () => boolean;\n  /** Register field config */\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst ValidationContext = createContext<ValidationContextValue | null>(null);\n\n/**\n * Props for ValidationProvider\n */\nexport interface ValidationProviderProps {\n  /** Custom validation functions from catalog */\n  customFunctions?: Record<string, ValidationFunction>;\n  children: ReactNode;\n}\n\n/**\n * Compare two DynamicValue args records shallowly.\n * Values are primitives or { $state: string }, so shallow comparison suffices.\n */\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    // Handle { $state: string } objects\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\n/**\n * Structural equality check for ValidationConfig.\n */\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n\n  // Compare validateOn\n  if (a.validateOn !== b.validateOn) return false;\n\n  // Compare checks arrays\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n\n  return true;\n}\n\n/**\n * Provider for validation\n */\nexport function ValidationProvider({\n  customFunctions = {},\n  children,\n}: ValidationProviderProps) {\n  const { state } = useStateStore();\n  const [fieldStates, setFieldStates] = useState<\n    Record<string, FieldValidationState>\n  >({});\n  const [fieldConfigs, setFieldConfigs] = useState<\n    Record<string, ValidationConfig>\n  >({});\n\n  const registerField = useCallback(\n    (path: string, config: ValidationConfig) => {\n      setFieldConfigs((prev) => {\n        const existing = prev[path];\n        // Bail out (return same reference) if config is unchanged to avoid\n        // infinite re-render loops when callers pass a fresh object each render.\n        if (existing && validationConfigEqual(existing, config)) {\n          return prev;\n        }\n        return { ...prev, [path]: config };\n      });\n    },\n    [],\n  );\n\n  const validate = useCallback(\n    (path: string, config: ValidationConfig): ValidationResult => {\n      // Walk the nested state object using JSON Pointer segments\n      const segments = path.split(\"/\").filter(Boolean);\n      let value: unknown = state;\n      for (const seg of segments) {\n        if (value != null && typeof value === \"object\") {\n          value = (value as Record<string, unknown>)[seg];\n        } else {\n          value = undefined;\n          break;\n        }\n      }\n      const result = runValidation(config, {\n        value,\n        stateModel: state,\n        customFunctions,\n      });\n\n      setFieldStates((prev) => ({\n        ...prev,\n        [path]: {\n          touched: prev[path]?.touched ?? true,\n          validated: true,\n          result,\n        },\n      }));\n\n      return result;\n    },\n    [state, customFunctions],\n  );\n\n  const touch = useCallback((path: string) => {\n    setFieldStates((prev) => ({\n      ...prev,\n      [path]: {\n        ...prev[path],\n        touched: true,\n        validated: prev[path]?.validated ?? false,\n        result: prev[path]?.result ?? null,\n      },\n    }));\n  }, []);\n\n  const clear = useCallback((path: string) => {\n    setFieldStates((prev) => {\n      const { [path]: _, ...rest } = prev;\n      return rest;\n    });\n  }, []);\n\n  const validateAll = useCallback(() => {\n    let allValid = true;\n\n    for (const [path, config] of Object.entries(fieldConfigs)) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n\n    return allValid;\n  }, [fieldConfigs, validate]);\n\n  const value = useMemo<ValidationContextValue>(\n    () => ({\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    }),\n    [\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    ],\n  );\n\n  return (\n    <ValidationContext.Provider value={value}>\n      {children}\n    </ValidationContext.Provider>\n  );\n}\n\n/**\n * Hook to access validation context\n */\nexport function useValidation(): ValidationContextValue {\n  const ctx = useContext(ValidationContext);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to get validation state for a field\n */\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: FieldValidationState;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: string[];\n  isValid: boolean;\n} {\n  const {\n    fieldStates,\n    validate: validateField,\n    touch: touchField,\n    clear: clearField,\n    registerField,\n  } = useValidation();\n\n  // Register field on mount\n  React.useEffect(() => {\n    if (config) {\n      registerField(path, config);\n    }\n  }, [path, config, registerField]);\n\n  const state = fieldStates[path] ?? {\n    touched: false,\n    validated: false,\n    result: null,\n  };\n\n  const validate = useCallback(\n    () => validateField(path, config ?? { checks: [] }),\n    [path, config, validateField],\n  );\n\n  const touch = useCallback(() => touchField(path), [path, touchField]);\n  const clear = useCallback(() => clearField(path), [path, clearField]);\n\n  return {\n    state,\n    validate,\n    touch,\n    clear,\n    errors: state.result?.errors ?? [],\n    isValid: state.result?.valid ?? true,\n  };\n}\n"
  },
  {
    "path": "packages/react-native/src/contexts/visibility.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Visibility context value\n */\nexport interface VisibilityContextValue {\n  /** Evaluate a visibility condition */\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  /** The underlying visibility context */\n  ctx: CoreVisibilityContext;\n}\n\nconst VisibilityContext = createContext<VisibilityContextValue | null>(null);\n\n/**\n * Props for VisibilityProvider\n */\nexport interface VisibilityProviderProps {\n  children: ReactNode;\n}\n\n/**\n * Provider for visibility evaluation\n */\nexport function VisibilityProvider({ children }: VisibilityProviderProps) {\n  const { state } = useStateStore();\n\n  const ctx: CoreVisibilityContext = useMemo(\n    () => ({\n      stateModel: state,\n    }),\n    [state],\n  );\n\n  const isVisible = useMemo(\n    () => (condition: VisibilityCondition | undefined) =>\n      evaluateVisibility(condition, ctx),\n    [ctx],\n  );\n\n  const value = useMemo<VisibilityContextValue>(\n    () => ({ isVisible, ctx }),\n    [isVisible, ctx],\n  );\n\n  return (\n    <VisibilityContext.Provider value={value}>\n      {children}\n    </VisibilityContext.Provider>\n  );\n}\n\n/**\n * Hook to access visibility evaluation\n */\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = useContext(VisibilityContext);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to check if a condition is visible\n */\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): boolean {\n  const { isVisible } = useVisibility();\n  return isVisible(condition);\n}\n"
  },
  {
    "path": "packages/react-native/src/hooks.ts",
    "content": "import { useState, useCallback, useRef, useEffect } from \"react\";\nimport type {\n  Spec,\n  UIElement,\n  FlatElement,\n  JsonPatch,\n} from \"@json-render/core\";\nimport {\n  setByPath,\n  getByPath,\n  removeByPath,\n  validateSpec,\n  autoFixSpec,\n  formatSpecIssues,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./contexts/state\";\n\n// =============================================================================\n// useBoundProp — Two-way binding helper for $bindState/$bindItem expressions\n// =============================================================================\n\n/**\n * Hook for two-way bound props. Returns `[value, setValue]` where:\n *\n * - `value` is the already-resolved prop value (passed through from render props)\n * - `setValue` writes back to the bound state path (no-op if not bound)\n *\n * @example\n * ```tsx\n * const [value, setValue] = useBoundProp<string>(element.props.value, bindings?.value);\n * ```\n */\nexport function useBoundProp<T>(\n  propValue: T | undefined,\n  bindingPath: string | undefined,\n): [T | undefined, (value: T) => void] {\n  const { set } = useStateStore();\n  const setValue = useCallback(\n    (value: T) => {\n      if (bindingPath) set(bindingPath, value);\n    },\n    [bindingPath, set],\n  );\n  return [propValue, setValue];\n}\n\n/**\n * Result of attempting to parse a JSONL line.\n * - `patch`: successfully parsed patch (or null)\n * - `malformed`: true only if the line looked like JSON (starts with `{`)\n *   but could not be parsed. Plain text commentary is NOT malformed.\n */\ninterface ParseResult {\n  patch: JsonPatch | null;\n  malformed: boolean;\n}\n\n/**\n * Check if a line looks like it's attempting to be JSON.\n * LLMs often output commentary text before/between patches — those\n * lines should be skipped, not treated as malformed.\n */\nfunction looksLikeJson(line: string): boolean {\n  return line.startsWith(\"{\") || line.startsWith(\"[\");\n}\n\n/**\n * Parse a single JSON patch line.\n * Includes recovery for common LLM JSON errors like trailing extra braces.\n *\n * Returns a ParseResult so the caller can distinguish between:\n * - Successfully parsed patch\n * - Commentary text (skip silently)\n * - Genuinely malformed JSON (trigger retry)\n */\nfunction parsePatchLine(line: string): ParseResult {\n  const trimmed = line.trim();\n  if (!trimmed || trimmed.startsWith(\"//\")) {\n    return { patch: null, malformed: false };\n  }\n\n  // If it doesn't look like JSON at all, it's commentary — skip it\n  if (!looksLikeJson(trimmed)) {\n    return { patch: null, malformed: false };\n  }\n\n  // Try parsing as-is first\n  try {\n    return { patch: JSON.parse(trimmed) as JsonPatch, malformed: false };\n  } catch {\n    // Fall through to recovery\n  }\n\n  // Recovery: strip trailing extra braces/brackets one at a time\n  // LLMs commonly generate extra closing characters in nested JSON\n  let attempt = trimmed;\n  for (let i = 0; i < 3; i++) {\n    const last = attempt[attempt.length - 1];\n    if (last === \"}\" || last === \"]\") {\n      attempt = attempt.slice(0, -1);\n      try {\n        const result = JSON.parse(attempt) as JsonPatch;\n        console.warn(\n          `[json-render] Recovered malformed JSONL line by removing ${i + 1} trailing '${last}'`,\n        );\n        return { patch: result, malformed: false };\n      } catch {\n        // Keep stripping\n      }\n    } else {\n      break;\n    }\n  }\n\n  // Looks like JSON but couldn't parse — genuinely malformed\n  return { patch: null, malformed: true };\n}\n\n/**\n * Set a value at a spec path (for add/replace operations).\n */\nfunction setSpecValue(newSpec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    const statePath = path.slice(\"/state\".length);\n    setByPath(newSpec.state as Record<string, unknown>, statePath, value);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      newSpec.elements[elementKey] = value as UIElement;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Remove a value at a spec path.\n */\nfunction removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    newSpec.state = undefined;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    const statePath = path.slice(\"/state\".length);\n    removeByPath(newSpec.state as Record<string, unknown>, statePath);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      const { [elementKey]: _, ...rest } = newSpec.elements;\n      newSpec.elements = rest;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Get a value at a spec path.\n */\nfunction getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\n/**\n * Apply an RFC 6902 JSON patch to the current spec.\n * Supports add, remove, replace, move, copy, and test operations.\n */\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = { ...spec, elements: { ...spec.elements } };\n\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\": {\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    }\n    case \"remove\": {\n      removeSpecValue(newSpec, patch.path);\n      break;\n    }\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getSpecValue(newSpec, patch.from);\n      removeSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      // test is a no-op for rendering purposes (validation only)\n      break;\n    }\n  }\n\n  return newSpec;\n}\n\n// =============================================================================\n// Stream result types\n// =============================================================================\n\n/** Result of a single stream request */\ninterface StreamResult {\n  /** The spec after applying all successfully parsed patches */\n  spec: Spec;\n  /** Whether the stream completed naturally (vs. being aborted) */\n  completed: boolean;\n  /** Malformed lines that could not be parsed (even after recovery) */\n  malformedLines: string[];\n}\n\n/**\n * Options for useUIStream\n */\nexport interface UseUIStreamOptions {\n  /** API endpoint */\n  api: string;\n  /** Callback when complete */\n  onComplete?: (spec: Spec) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n  /**\n   * Custom fetch implementation with ReadableStream support.\n   *\n   * React Native's built-in fetch does not support `response.body`\n   * (ReadableStream). Pass a streaming-capable fetch here, e.g.\n   * `import { fetch } from 'expo/fetch'`.\n   *\n   * Falls back to the global `fetch` if not provided.\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  fetch?: (url: string, init?: any) => Promise<Response>;\n  /**\n   * Enable validation and auto-repair.\n   *\n   * When true:\n   * - **Mid-stream**: Each JSONL line is validated as it arrives. If a line\n   *   is malformed JSON (and recovery fails), the stream is aborted\n   *   immediately and a repair prompt is sent to continue generation.\n   * - **Post-stream**: After the stream completes, structural validation\n   *   runs (missing children, visible-in-props, etc.). Issues that can be\n   *   auto-fixed are fixed locally; remaining errors trigger a repair prompt.\n   *\n   * Defaults to false.\n   */\n  validate?: boolean;\n  /**\n   * Maximum number of automatic repair retries (covers both mid-stream\n   * and post-stream retries combined). Defaults to 5.\n   */\n  maxRetries?: number;\n}\n\n/**\n * Return type for useUIStream\n */\nexport interface UseUIStreamReturn {\n  /** Current UI spec */\n  spec: Spec | null;\n  /** Whether currently streaming */\n  isStreaming: boolean;\n  /** Error if any */\n  error: Error | null;\n  /** Raw JSONL lines received from the stream */\n  rawLines: string[];\n  /** Send a prompt to generate UI */\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  /** Stop the current generation */\n  stop: () => void;\n  /** Clear the current spec */\n  clear: () => void;\n}\n\n/**\n * Hook for streaming UI generation\n */\nexport function useUIStream({\n  api,\n  onComplete,\n  onError,\n  fetch: fetchFn = globalThis.fetch,\n  validate: enableValidation = false,\n  maxRetries = 5,\n}: UseUIStreamOptions): UseUIStreamReturn {\n  const [spec, setSpec] = useState<Spec | null>(null);\n  const [isStreaming, setIsStreaming] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n  const [rawLines, setRawLines] = useState<string[]>([]);\n  const abortControllerRef = useRef<AbortController | null>(null);\n\n  const stop = useCallback(() => {\n    abortControllerRef.current?.abort();\n    abortControllerRef.current = null;\n    setIsStreaming(false);\n  }, []);\n\n  const clear = useCallback(() => {\n    setSpec(null);\n    setError(null);\n    setRawLines([]);\n  }, []);\n\n  /**\n   * Stream a single request. Returns the result including whether the\n   * stream completed and any malformed lines encountered.\n   *\n   * When `abortOnMalformed` is true, the stream is aborted on the first\n   * malformed line so the caller can retry immediately.\n   */\n  const streamRequest = useCallback(\n    async (\n      prompt: string,\n      context: Record<string, unknown> | undefined,\n      initialSpec: Spec,\n      abortOnMalformed: boolean,\n    ): Promise<StreamResult> => {\n      let currentSpec = initialSpec;\n      setSpec(currentSpec);\n      const malformedLines: string[] = [];\n\n      const response = await fetchFn(api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          prompt,\n          context,\n          currentSpec,\n        }),\n        signal: abortControllerRef.current!.signal,\n      });\n\n      if (!response.ok) {\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) {\n            errorMessage = errorData.message;\n          } else if (errorData.error) {\n            errorMessage = errorData.error;\n          }\n        } catch {\n          // Ignore JSON parsing errors, use default message\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) {\n        throw new Error(\"No response body\");\n      }\n\n      const decoder = new TextDecoder();\n      let buffer = \"\";\n      let aborted = false;\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) {\n          break;\n        }\n\n        const decoded = decoder.decode(value, { stream: true });\n        buffer += decoded;\n\n        // Process complete lines\n        const lines = buffer.split(\"\\n\");\n        buffer = lines.pop() ?? \"\";\n\n        const newLines: string[] = [];\n        for (const line of lines) {\n          if (line.trim()) {\n            newLines.push(line);\n          }\n          const { patch, malformed } = parsePatchLine(line);\n          if (patch) {\n            currentSpec = applyPatch(currentSpec, patch);\n            setSpec({ ...currentSpec });\n          } else if (malformed) {\n            // Genuinely malformed JSON (started with { but couldn't parse)\n            malformedLines.push(line.trim());\n\n            if (abortOnMalformed) {\n              await reader.cancel();\n              aborted = true;\n              break;\n            }\n          }\n          // else: commentary text — skip silently\n        }\n\n        if (aborted) break;\n\n        if (newLines.length > 0) {\n          setRawLines((prev) => [...prev, ...newLines]);\n        }\n      }\n\n      // Process any remaining buffer (only if stream completed naturally)\n      if (!aborted && buffer.trim()) {\n        setRawLines((prev) => [...prev, buffer]);\n        const { patch, malformed } = parsePatchLine(buffer);\n        if (patch) {\n          currentSpec = applyPatch(currentSpec, patch);\n          setSpec({ ...currentSpec });\n        } else if (malformed) {\n          malformedLines.push(buffer.trim());\n        }\n      }\n\n      return { spec: currentSpec, completed: !aborted, malformedLines };\n    },\n    [api, fetchFn],\n  );\n\n  const send = useCallback(\n    async (prompt: string, context?: Record<string, unknown>) => {\n      // Abort any existing request\n      abortControllerRef.current?.abort();\n      abortControllerRef.current = new AbortController();\n\n      setIsStreaming(true);\n      setError(null);\n      setRawLines([]);\n\n      // Start with previous spec if provided, otherwise empty spec\n      const previousSpec = context?.previousSpec as Spec | undefined;\n      let currentSpec: Spec =\n        previousSpec && previousSpec.root\n          ? { ...previousSpec, elements: { ...previousSpec.elements } }\n          : { root: \"\", elements: {} };\n\n      let retriesUsed = 0;\n      let currentPrompt = prompt;\n      let currentContext = context;\n\n      try {\n        // Retry loop handles both mid-stream (malformed JSON) and\n        // post-stream (structural validation) repairs.\n        while (retriesUsed <= maxRetries) {\n          const result = await streamRequest(\n            currentPrompt,\n            currentContext,\n            currentSpec,\n            enableValidation, // only abort on malformed when validation is on\n          );\n          currentSpec = result.spec;\n\n          // ---------------------------------------------------------------\n          // Mid-stream repair: stream was aborted due to malformed line\n          // ---------------------------------------------------------------\n          if (!result.completed && result.malformedLines.length > 0) {\n            if (retriesUsed >= maxRetries) {\n              break;\n            }\n            retriesUsed++;\n\n            // Build a repair prompt that asks the AI to continue from\n            // the current partial spec\n            currentContext = { ...context, previousSpec: currentSpec };\n            currentPrompt =\n              `The previous generation contained malformed JSON that could not be parsed. The line was:\\n` +\n              `${result.malformedLines[result.malformedLines.length - 1]?.slice(0, 500)}\\n\\n` +\n              `The current spec state is provided. Continue generating from where you left off. ` +\n              `Output ONLY the remaining patches needed to complete the UI.`;\n            continue;\n          }\n\n          // ---------------------------------------------------------------\n          // Post-stream: validation is off or spec is empty → done\n          // ---------------------------------------------------------------\n          if (!enableValidation || !currentSpec.root) {\n            break;\n          }\n\n          // ---------------------------------------------------------------\n          // Post-stream: auto-fix deterministic issues (no retry needed)\n          // ---------------------------------------------------------------\n          const { spec: fixedSpec, fixes } = autoFixSpec(currentSpec);\n          if (fixes.length > 0) {\n            currentSpec = fixedSpec;\n            setSpec({ ...currentSpec });\n          }\n\n          // ---------------------------------------------------------------\n          // Post-stream: structural validation\n          // ---------------------------------------------------------------\n          const validation = validateSpec(currentSpec);\n          if (validation.valid) {\n            break;\n          }\n\n          // Still has errors\n          const errors = validation.issues.filter(\n            (i) => i.severity === \"error\",\n          );\n          if (retriesUsed >= maxRetries) {\n            break;\n          }\n\n          retriesUsed++;\n          const issueText = formatSpecIssues(validation.issues);\n\n          currentContext = { ...context, previousSpec: currentSpec };\n          currentPrompt =\n            `FIX THE FOLLOWING ERRORS in the current UI spec. Output ONLY the patches needed to fix these issues, do not recreate the entire UI.\\n\\n` +\n            issueText;\n          // continue loop\n        }\n\n        onComplete?.(currentSpec);\n      } catch (err) {\n        if ((err as Error).name === \"AbortError\") {\n          return;\n        }\n        const error = err instanceof Error ? err : new Error(String(err));\n        setError(error);\n        onError?.(error);\n      } finally {\n        setIsStreaming(false);\n      }\n    },\n    [\n      api,\n      fetchFn,\n      onComplete,\n      onError,\n      enableValidation,\n      maxRetries,\n      streamRequest,\n    ],\n  );\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      abortControllerRef.current?.abort();\n    };\n  }, []);\n\n  return {\n    spec,\n    isStreaming,\n    error,\n    rawLines,\n    send,\n    stop,\n    clear,\n  };\n}\n\n/**\n * Convert a flat element list to a Spec.\n * Input elements use key/parentKey to establish identity and relationships.\n * Output spec uses the map-based format where key is the map entry key\n * and parent-child relationships are expressed through children arrays.\n */\nexport function flatToTree(elements: FlatElement[]): Spec {\n  const elementMap: Record<string, UIElement> = {};\n  let root = \"\";\n\n  // First pass: add all elements to map\n  for (const element of elements) {\n    elementMap[element.key] = {\n      type: element.type,\n      props: element.props,\n      children: [],\n      visible: element.visible,\n    };\n  }\n\n  // Second pass: build parent-child relationships\n  for (const element of elements) {\n    if (element.parentKey) {\n      const parent = elementMap[element.parentKey];\n      if (parent) {\n        if (!parent.children) {\n          parent.children = [];\n        }\n        parent.children.push(element.key);\n      }\n    } else {\n      root = element.key;\n    }\n  }\n\n  return { root, elements: elementMap };\n}\n"
  },
  {
    "path": "packages/react-native/src/index.ts",
    "content": "// Contexts\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./contexts/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n  type VisibilityProviderProps,\n} from \"./contexts/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./contexts/actions\";\n\nexport {\n  ValidationProvider,\n  useValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./contexts/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/repeat-scope\";\n\n// Schema (React Native's spec format)\nexport {\n  schema,\n  type ReactNativeSchema,\n  type ReactNativeSpec,\n  // Backward compatibility\n  elementTreeSchema,\n  type ElementTreeSchema,\n  type ElementTreeSpec,\n} from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec, StateStore } from \"@json-render/core\";\nexport { createStateStore } from \"@json-render/core\";\n\n// Catalog-aware types for React Native\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n  ActionFn,\n  Actions,\n} from \"./catalog-types\";\n\n// Renderer\nexport {\n  // Registry\n  defineRegistry,\n  type DefineRegistryResult,\n  // createRenderer (higher-level, includes providers)\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  // Low-level\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRenderer,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n  // Standard components\n  standardComponents,\n  createStandardActionHandlers,\n} from \"./renderer\";\n\n// Hooks\nexport {\n  useUIStream,\n  useBoundProp,\n  flatToTree,\n  type UseUIStreamOptions,\n  type UseUIStreamReturn,\n} from \"./hooks\";\n\n// Catalog definitions\nexport {\n  standardComponentDefinitions,\n  standardActionDefinitions,\n  type ComponentDefinition,\n  type ActionDefinition,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/react-native/src/renderer.tsx",
    "content": "import React, {\n  type ComponentType,\n  type ErrorInfo,\n  type ReactNode,\n  useCallback,\n  useMemo,\n} from \"react\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  SchemaDefinition,\n  StateStore,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport type {\n  Components,\n  Actions,\n  ActionFn,\n  SetState,\n  StateModel,\n} from \"./catalog-types\";\nimport { useIsVisible, useVisibility } from \"./contexts/visibility\";\nimport { useActions } from \"./contexts/actions\";\nimport { useStateStore } from \"./contexts/state\";\nimport { StateProvider } from \"./contexts/state\";\nimport { VisibilityProvider } from \"./contexts/visibility\";\nimport { ActionProvider } from \"./contexts/actions\";\nimport { ValidationProvider } from \"./contexts/validation\";\nimport { ConfirmDialog } from \"./contexts/actions\";\nimport { standardComponents } from \"./components/standard\";\nimport { RepeatScopeProvider, useRepeatScope } from \"./contexts/repeat-scope\";\n\n/**\n * Props passed to component renderers\n */\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  /** The element being rendered */\n  element: UIElement<string, P>;\n  /** Rendered children */\n  children?: ReactNode;\n  /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. Always provided by the renderer. */\n  emit: (event: string) => void;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   * Only present when at least one prop uses `{ $bindState: \"...\" }` or `{ $bindItem: \"...\" }`.\n   */\n  bindings?: Record<string, string>;\n  /** Whether the parent is loading */\n  loading?: boolean;\n}\n\n/**\n * Component renderer type\n */\nexport type ComponentRenderer<P = Record<string, unknown>> = ComponentType<\n  ComponentRenderProps<P>\n>;\n\n/**\n * Registry of component renderers\n */\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\n/**\n * Props for the Renderer component\n */\nexport interface RendererProps {\n  /** The UI spec to render */\n  spec: Spec | null;\n  /**\n   * Component registry. If omitted, only standard components are used.\n   * When provided, custom components are merged with (and override) standard components.\n   */\n  registry?: ComponentRegistry;\n  /** Whether to include standard components (default: true) */\n  includeStandard?: boolean;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n// ---------------------------------------------------------------------------\n// ElementErrorBoundary – catches rendering errors in individual elements so\n// a single bad component never crashes the whole page.\n// ---------------------------------------------------------------------------\n\ninterface ElementErrorBoundaryProps {\n  elementType: string;\n  children: ReactNode;\n}\n\ninterface ElementErrorBoundaryState {\n  hasError: boolean;\n}\n\nclass ElementErrorBoundary extends React.Component<\n  ElementErrorBoundaryProps,\n  ElementErrorBoundaryState\n> {\n  constructor(props: ElementErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ElementErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    console.error(\n      `[json-render] Rendering error in <${this.props.elementType}>:`,\n      error,\n      info.componentStack,\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      // Render nothing – the element silently disappears rather than\n      // crashing the entire application.\n      return null;\n    }\n    return this.props.children;\n  }\n}\n\ninterface ElementRendererProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Element renderer component.\n * Memoized to prevent re-rendering all repeat children when state changes.\n */\nconst ElementRenderer = React.memo(function ElementRenderer({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: ElementRendererProps) {\n  const repeatScope = useRepeatScope();\n  const { ctx } = useVisibility();\n  const { execute } = useActions();\n  const { getSnapshot } = useStateStore();\n\n  // Build context with repeat scope (used for both visibility and props)\n  const fullCtx: PropResolutionContext = useMemo(\n    () =>\n      repeatScope\n        ? {\n            ...ctx,\n            repeatItem: repeatScope.item,\n            repeatIndex: repeatScope.index,\n            repeatBasePath: repeatScope.basePath,\n          }\n        : ctx,\n    [ctx, repeatScope],\n  );\n\n  // Evaluate visibility (now supports $item/$index inside repeat scopes)\n  const isVisible =\n    element.visible === undefined\n      ? true\n      : evaluateVisibility(element.visible, fullCtx);\n\n  // Create emit function that resolves events to action bindings.\n  // Must be called before any early return to satisfy Rules of Hooks.\n  const onBindings = element.on;\n  const emit = useCallback(\n    async (eventName: string) => {\n      const binding = onBindings?.[eventName];\n      if (!binding) return;\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      for (const b of actionBindings) {\n        if (!b.params) {\n          await execute(b);\n          continue;\n        }\n        // Build a fresh context with live store state so that $state\n        // references in later actions see mutations from earlier ones.\n        const liveCtx: PropResolutionContext = {\n          ...fullCtx,\n          stateModel: getSnapshot(),\n        };\n        const resolved: Record<string, unknown> = {};\n        for (const [key, val] of Object.entries(b.params)) {\n          resolved[key] = resolveActionParam(val, liveCtx);\n        }\n        await execute({ ...b, params: resolved });\n      }\n    },\n    [onBindings, execute, fullCtx, getSnapshot],\n  );\n\n  // Don't render if not visible\n  if (!isVisible) {\n    return null;\n  }\n\n  // Resolve $bindState/$bindItem expressions → bindings map (prop name → state path)\n  const rawProps = element.props as Record<string, unknown>;\n  const elementBindings = resolveBindings(rawProps, fullCtx);\n\n  // Resolve dynamic prop expressions ($state, $item, $index, $bindState, $bindItem, $cond/$then/$else)\n  const resolvedProps = resolveElementProps(rawProps, fullCtx);\n\n  const resolvedElement =\n    resolvedProps !== element.props\n      ? { ...element, props: resolvedProps }\n      : element;\n\n  // Get the component renderer\n  const Component = registry[resolvedElement.type] ?? fallback;\n\n  if (!Component) {\n    console.warn(`No renderer for component type: ${resolvedElement.type}`);\n    return null;\n  }\n\n  // ---- Render children (with repeat support) ----\n  const children = resolvedElement.repeat ? (\n    <RepeatChildren\n      element={resolvedElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  ) : (\n    resolvedElement.children?.map((childKey) => {\n      const childElement = spec.elements[childKey];\n      if (!childElement) {\n        if (!loading) {\n          console.warn(\n            `[json-render] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\". This element will not render.`,\n          );\n        }\n        return null;\n      }\n      return (\n        <ElementRenderer\n          key={childKey}\n          element={childElement}\n          spec={spec}\n          registry={registry}\n          loading={loading}\n          fallback={fallback}\n        />\n      );\n    })\n  );\n\n  return (\n    <ElementErrorBoundary elementType={resolvedElement.type}>\n      <Component\n        element={resolvedElement}\n        emit={emit}\n        bindings={elementBindings}\n        loading={loading}\n      >\n        {children}\n      </Component>\n    </ElementErrorBoundary>\n  );\n});\n\n// ---------------------------------------------------------------------------\n// RepeatChildren -- renders child elements once per item in a state array.\n// Used when an element has a `repeat` field.\n// ---------------------------------------------------------------------------\n\nfunction RepeatChildren({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}) {\n  const { state } = useStateStore();\n  const repeat = element.repeat!;\n  const statePath = repeat.statePath;\n\n  const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];\n\n  return (\n    <>\n      {items.map((itemValue, index) => {\n        // Use a stable key: prefer key field, fall back to index\n        const key =\n          repeat.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String(\n                (itemValue as Record<string, unknown>)[repeat.key] ?? index,\n              )\n            : String(index);\n\n        return (\n          <RepeatScopeProvider\n            key={key}\n            item={itemValue}\n            index={index}\n            basePath={`${statePath}/${index}`}\n          >\n            {element.children?.map((childKey) => {\n              const childElement = spec.elements[childKey];\n              if (!childElement) {\n                if (!loading) {\n                  console.warn(\n                    `[json-render] Missing element \"${childKey}\" referenced as child of \"${element.type}\" (repeat). This element will not render.`,\n                  );\n                }\n                return null;\n              }\n              return (\n                <ElementRenderer\n                  key={childKey}\n                  element={childElement}\n                  spec={spec}\n                  registry={registry}\n                  loading={loading}\n                  fallback={fallback}\n                />\n              );\n            })}\n          </RepeatScopeProvider>\n        );\n      })}\n    </>\n  );\n}\n\n/**\n * Main renderer component.\n *\n * By default, standard React Native components are included.\n * Custom components in `registry` override standard ones with the same name.\n *\n * @example\n * ```tsx\n * // Use standard components only\n * <Renderer spec={spec} />\n *\n * // Add/override components\n * <Renderer spec={spec} registry={{ CustomCard: MyCard }} />\n *\n * // Disable standard components entirely\n * <Renderer spec={spec} registry={myRegistry} includeStandard={false} />\n * ```\n */\nexport function Renderer({\n  spec,\n  registry: customRegistry,\n  includeStandard = true,\n  loading,\n  fallback,\n}: RendererProps) {\n  // Merge standard + custom components (custom overrides standard)\n  const registry: ComponentRegistry = useMemo(\n    () => ({\n      ...(includeStandard ? standardComponents : {}),\n      ...customRegistry,\n    }),\n    [customRegistry, includeStandard],\n  );\n\n  if (!spec || !spec.root) {\n    return null;\n  }\n\n  const rootElement = spec.elements[spec.root];\n  if (!rootElement) {\n    return null;\n  }\n\n  return (\n    <ElementRenderer\n      element={rootElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  );\n}\n\n// Re-export standard components and action handlers\nexport {\n  standardComponents,\n  createStandardActionHandlers,\n} from \"./components/standard\";\n\n/**\n * Props for JSONUIProvider\n */\nexport interface JSONUIProviderProps {\n  /**\n   * Component registry. If omitted, only standard components are used.\n   * Custom components are merged with (and override) standard components.\n   */\n  registry?: ComponentRegistry;\n  /**\n   * External store (controlled mode). When provided, `initialState` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** Initial state model (uncontrolled mode) */\n  initialState?: Record<string, unknown>;\n  /** Action handlers */\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  /** Navigation function */\n  navigate?: (path: string) => void;\n  /** Custom validation functions */\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  children: ReactNode;\n}\n\n/**\n * Combined provider for all JSONUI contexts\n */\nexport function JSONUIProvider({\n  registry,\n  store,\n  initialState,\n  handlers,\n  navigate,\n  validationFunctions,\n  onStateChange,\n  children,\n}: JSONUIProviderProps) {\n  return (\n    <StateProvider\n      store={store}\n      initialState={initialState}\n      onStateChange={onStateChange}\n    >\n      <VisibilityProvider>\n        <ActionProvider handlers={handlers} navigate={navigate}>\n          <ValidationProvider customFunctions={validationFunctions}>\n            {children}\n            <ConfirmationDialogManager />\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n/**\n * Renders the confirmation dialog when needed\n */\nfunction ConfirmationDialogManager() {\n  const { pendingConfirmation, confirm, cancel } = useActions();\n\n  if (!pendingConfirmation?.action.confirm) {\n    return null;\n  }\n\n  return (\n    <ConfirmDialog\n      confirm={pendingConfirmation.action.confirm}\n      onConfirm={confirm}\n      onCancel={cancel}\n    />\n  );\n}\n\n// ============================================================================\n// defineRegistry\n// ============================================================================\n\n/**\n * Result returned by defineRegistry\n */\nexport interface DefineRegistryResult {\n  /** Component registry for `<Renderer registry={...} />` */\n  registry: ComponentRegistry;\n  /**\n   * Create ActionProvider-compatible handlers.\n   * Accepts getter functions so handlers always read the latest state/setState\n   * (e.g. from React refs).\n   */\n  handlers: (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;\n  /**\n   * Execute an action by name imperatively\n   * (for use outside the React tree, e.g. initial state loading).\n   */\n  executeAction: (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state?: StateModel,\n  ) => Promise<void>;\n}\n\n/**\n * Create a registry from a catalog with components and/or actions.\n *\n * @example\n * ```tsx\n * // Components only\n * const { registry } = defineRegistry(catalog, {\n *   components: {\n *     Card: ({ props, children }) => (\n *       <View style={styles.card}><Text>{props.title}</Text>{children}</View>\n *     ),\n *   },\n * });\n *\n * // Actions only\n * const { handlers, executeAction } = defineRegistry(catalog, {\n *   actions: {\n *     viewCustomers: async (params, setState) => { ... },\n *   },\n * });\n *\n * // Both\n * const { registry, handlers, executeAction } = defineRegistry(catalog, {\n *   components: { ... },\n *   actions: { ... },\n * });\n * ```\n */\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: {\n    components?: Components<C>;\n    actions?: Actions<C>;\n  },\n): DefineRegistryResult {\n  // Build component registry\n  const registry: ComponentRegistry = {};\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = ({\n        element,\n        children,\n        emit,\n        bindings,\n        loading,\n      }: ComponentRenderProps) => {\n        return (componentFn as DefineRegistryComponentFn)({\n          props: element.props,\n          children,\n          emit,\n          bindings,\n          loading,\n        });\n      };\n    }\n  }\n\n  // Build action helpers\n  const actionMap = options.actions\n    ? (Object.entries(options.actions) as Array<\n        [string, DefineRegistryActionFn]\n      >)\n    : [];\n\n  const handlers = (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {\n    const result: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void>\n    > = {};\n    for (const [name, actionFn] of actionMap) {\n      result[name] = async (params) => {\n        const setState = getSetState();\n        const state = getState();\n        if (setState) {\n          await actionFn(params, setState, state);\n        }\n      };\n    }\n    return result;\n  };\n\n  const executeAction = async (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state: StateModel = {},\n  ): Promise<void> => {\n    const entry = actionMap.find(([name]) => name === actionName);\n    if (entry) {\n      await entry[1](params, setState, state);\n    } else {\n      console.warn(`Unknown action: ${actionName}`);\n    }\n  };\n\n  return { registry, handlers, executeAction };\n}\n\n/** @internal */\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: React.ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => React.ReactNode;\n\n/** @internal */\ntype DefineRegistryActionFn = (\n  params: Record<string, unknown> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n// ============================================================================\n// NEW API\n// ============================================================================\n\n/**\n * Props for renderers created with createRenderer\n */\nexport interface CreateRendererProps {\n  /** The spec to render (AI-generated JSON) */\n  spec: Spec | null;\n  /**\n   * External store (controlled mode). When provided, `state` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** State context for dynamic values (uncontrolled mode) */\n  state?: Record<string, unknown>;\n  /** Action handler */\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Component map type - maps component names to React Native components\n */\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: ComponentType<\n    ComponentRenderProps<\n      TComponents[K][\"props\"] extends { _output: infer O }\n        ? O\n        : Record<string, unknown>\n    >\n  >;\n};\n\n/**\n * Create a renderer from a catalog\n *\n * @example\n * ```typescript\n * const DashboardRenderer = createRenderer(dashboardCatalog, {\n *   Card: ({ element, children }) => <View style={styles.card}>{children}</View>,\n *   Metric: ({ element }) => <Text>{element.props.value}</Text>,\n * });\n *\n * // Usage\n * <DashboardRenderer spec={aiGeneratedSpec} state={state} />\n * ```\n */\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentType<CreateRendererProps> {\n  // Convert component map to registry\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  // Return the renderer component\n  return function CatalogRenderer({\n    spec,\n    store,\n    state,\n    onAction,\n    onStateChange,\n    loading,\n    fallback,\n  }: CreateRendererProps) {\n    // Wrap onAction with a Proxy so any action name routes to the callback\n    const actionHandlers = onAction\n      ? new Proxy(\n          {} as Record<\n            string,\n            (params: Record<string, unknown>) => void | Promise<void>\n          >,\n          {\n            get: (_target, prop: string) => {\n              return (params: Record<string, unknown>) =>\n                onAction(prop, params);\n            },\n            has: () => true,\n          },\n        )\n      : undefined;\n\n    return (\n      <StateProvider\n        store={store}\n        initialState={state}\n        onStateChange={onStateChange}\n      >\n        <VisibilityProvider>\n          <ActionProvider handlers={actionHandlers}>\n            <ValidationProvider>\n              <Renderer\n                spec={spec}\n                registry={registry}\n                loading={loading}\n                fallback={fallback}\n              />\n              <ConfirmationDialogManager />\n            </ValidationProvider>\n          </ActionProvider>\n        </VisibilityProvider>\n      </StateProvider>\n    );\n  };\n}\n"
  },
  {
    "path": "packages/react-native/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/react-native\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like\n    spec: s.object({\n      /** Root element key */\n      root: s.string(),\n      /** Flat map of elements by key */\n      elements: s.record(\n        s.object({\n          /** Component type from catalog */\n          type: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Child element keys (flat reference) */\n          children: s.array(s.string()),\n          /** Visibility condition */\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Component definitions */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n        slots: s.array(s.string()),\n        /** Description for AI generation hints */\n        description: s.string(),\n        /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n        example: s.any(),\n      }),\n      /** Action definitions (optional) */\n      actions: s.map({\n        /** Zod schema for action params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    defaultRules: [\n      // Layout patterns\n      \"FIXED BOTTOM BAR PATTERN: When building a screen with a fixed header and/or fixed bottom tab bar, the outermost vertical layout component must have flex:1 so it fills the screen. The scrollable content area must also have flex:1. Structure: screen wrapper > vertical layout(flex:1, gap:0) > [header, content wrapper(flex:1) > [scroll container(...)], bottom-tabs]. Both the outer layout AND the content wrapper need flex:1. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"NEVER place a bottom tab bar or fixed footer inside a scroll container. It must be a sibling AFTER the flex:1 container that holds the scroll content.\",\n\n      // Element integrity\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n      \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n      'When building repeating content backed by a state array (e.g. todos, posts, cart items), use the \"repeat\" field on a container element from the AVAILABLE COMPONENTS list. Example: { \"type\": \"<ContainerComponent>\", \"props\": { \"gap\": 8 }, \"repeat\": { \"statePath\": \"/todos\", \"key\": \"id\" }, \"children\": [\"todo-item\"] }. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n      // Visible field placement\n      'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/activeTab\",\"eq\":\"home\"},\"children\":[...]}. WRONG: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{...},\"children\":[...]} with visible inside props.',\n\n      // Tab navigation pattern\n      \"TAB NAVIGATION PATTERN: When building a UI with multiple tabs, use a pressable/tappable component + setState action + visible conditions to make tabs functional. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      'Each tab button should be a pressable component wrapping its icon/label children, with action \"setState\" and actionParams { \"statePath\": \"/activeTab\", \"value\": \"tabName\" }.',\n      'Each tab\\'s content section should have a visible condition: { \"$state\": \"/activeTab\", \"eq\": \"tabName\" }.',\n      \"The first tab's content should NOT have a visible condition (so it shows by default when no tab is selected yet). All other tabs MUST have a visible condition.\",\n\n      // Tab active state highlighting (using dynamic props)\n      \"TAB ACTIVE STYLING: Use $cond dynamic props on icon elements inside each tab button so a single icon changes appearance based on the active tab.\",\n      '  - For the icon name: { \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }, \"$then\": \"home\", \"$else\": \"home-outline\" }',\n      '  - For the icon color: { \"$cond\": { \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }, \"$then\": \"#007AFF\", \"$else\": \"#8E8E93\" }',\n      \"  - For labels, use $cond on the color prop similarly.\",\n      '  - For the FIRST/DEFAULT tab, use { \"$cond\": [{ \"$state\": \"/activeTab\", \"eq\": \"thisTabName\" }], \"$then\": \"#007AFF\", \"$else\": \"#8E8E93\" } so it appears active before any tab is tapped. When no tab is selected yet, include a default tab with no visible condition.',\n\n      // Push/pop screen navigation (all screens in one spec)\n      'SCREEN NAVIGATION: Use a pressable component with action \"push\" and actionParams { \"screen\": \"screenName\" } to navigate to a new screen. Use action \"pop\" to go back. All screens must be defined in the SAME spec. ONLY use components from the AVAILABLE COMPONENTS list.',\n      'Each screen section uses a visible condition on /currentScreen: { \"$state\": \"/currentScreen\", \"eq\": \"screenName\" }. The default/home screen should show by default (no visible condition) and all other screens should have the appropriate visible condition.',\n      \"push automatically maintains a /navStack in the state model so pop always returns to the previous screen.\",\n      'Include a back button on pushed screens using action \"pop\". Example: pressable(action:\"pop\") > row layout > back icon + back label. ONLY use components from the AVAILABLE COMPONENTS list.',\n      \"Use push/pop for drill-down flows: tapping a list item to see details, opening a profile, etc. Use setState + visible conditions for tab switching within a screen.\",\n      'Example: A list screen with items that push to detail: a pressable component with action:\"push\" and actionParams:{screen:\"detail\"} wrapping each list item. The detail screen section has visible:{\"$state\":\"/currentScreen\",\"eq\":\"detail\"} and contains a back button with action:\"pop\". ONLY use components from the AVAILABLE COMPONENTS list.',\n    ],\n  },\n);\n\n/**\n * Type for the React Native schema\n */\nexport type ReactNativeSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type ReactNativeSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n\n// Backward compatibility aliases\n/** @deprecated Use `schema` instead */\nexport const elementTreeSchema = schema;\n/** @deprecated Use `ReactNativeSchema` instead */\nexport type ElementTreeSchema = ReactNativeSchema;\n/** @deprecated Use `ReactNativeSpec` instead */\nexport type ElementTreeSpec<T> = ReactNativeSpec<T>;\n"
  },
  {
    "path": "packages/react-native/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-native/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/schema.ts\", \"src/catalog.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: { resolve: [\"@internal/react-state\"] },\n  sourcemap: true,\n  clean: true,\n  noExternal: [\"@internal/react-state\"],\n  external: [\"react\", \"react-native\", \"@json-render/core\", \"zod\"],\n});\n"
  },
  {
    "path": "packages/react-pdf/CHANGELOG.md",
    "content": "# @json-render/react-pdf\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- b103676: Fix install failure caused by `@internal/react-state` (a private workspace package) being listed as a published dependency. The internal package is now bundled into each renderer's output at build time, so consumers no longer need to resolve it from npm.\n  - @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @internal/react-state@0.8.1\n\n## 0.8.0\n\n### Minor Changes\n\n- 09376db: New `@json-render/react-pdf` package for generating PDF documents from JSON specs.\n\n  ### New: `@json-render/react-pdf`\n\n  PDF renderer for json-render, powered by `@react-pdf/renderer`. Define catalogs and registries the same way as `@json-render/react`, but output PDF documents instead of web UI.\n  - `renderToBuffer(spec)` — render a spec to an in-memory PDF buffer\n  - `renderToStream(spec)` — render to a readable stream (pipe to HTTP response)\n  - `renderToFile(spec, path)` — render directly to a file on disk\n  - `defineRegistry` / `createRenderer` — same API as `@json-render/react` for custom components\n  - `standardComponentDefinitions` — Zod-based catalog definitions (server-safe via `@json-render/react-pdf/catalog`)\n  - `standardComponents` — React PDF implementations for all standard components\n  - Server-safe import via `@json-render/react-pdf/server`\n\n  Standard components:\n  - **Document structure**: Document, Page\n  - **Layout**: View, Row, Column\n  - **Content**: Heading, Text, Image, Link\n  - **Data**: Table, List\n  - **Decorative**: Divider, Spacer\n  - **Page-level**: PageNumber\n\n  Includes full context support: state management, visibility conditions, actions, validation, and repeat scopes — matching the capabilities of `@json-render/react`.\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n"
  },
  {
    "path": "packages/react-pdf/README.md",
    "content": "# @json-render/react-pdf\n\nReact PDF renderer for `@json-render/core`. Generate PDF documents from JSON specs using `@react-pdf/renderer`.\n\n## Install\n\n```bash\nnpm install @json-render/core @json-render/react-pdf\n```\n\n## Quick Start\n\n### Render a spec to a PDF buffer\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### With a 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({\n        label: z.string(),\n        color: z.string().nullable(),\n      }),\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, borderRadius: 4 }}>\n        <Text style={{ fontSize: 10 }}>{props.label}</Text>\n      </View>\n    ),\n  },\n});\n\nconst buffer = await renderToBuffer(spec, { registry });\n```\n\n## Standard Components\n\n### Document Structure\n\n| Component | Description |\n|-----------|-------------|\n| `Document` | Top-level PDF wrapper. Must be the root element. |\n| `Page` | A page with size (A4, LETTER, etc.), orientation, and margins. |\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `View` | Generic container with padding, margin, background, border. |\n| `Row` | Horizontal flex layout with gap, align, justify. |\n| `Column` | Vertical flex layout with gap, align, justify. |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | h1-h4 heading text. |\n| `Text` | Body text with fontSize, color, weight, alignment. |\n| `Image` | Image from URL or base64. |\n| `Link` | Hyperlink with text and href. |\n\n### Data\n\n| Component | Description |\n|-----------|-------------|\n| `Table` | Data table with typed columns and string rows. |\n| `List` | Ordered or unordered list. |\n\n### Decorative\n\n| Component | Description |\n|-----------|-------------|\n| `Divider` | Horizontal line separator. |\n| `Spacer` | Empty vertical space. |\n\n### Page-Level\n\n| Component | Description |\n|-----------|-------------|\n| `PageNumber` | Renders current page number and total pages. |\n\n## Server-Side APIs\n\n```typescript\nimport { renderToBuffer, renderToStream, renderToFile } from \"@json-render/react-pdf\";\n\n// Render to an in-memory Buffer\nconst buffer = await renderToBuffer(spec);\n\n// Render to a readable stream (pipe to HTTP response)\nconst stream = await renderToStream(spec);\nstream.pipe(res);\n\n// Render directly to a file\nawait renderToFile(spec, \"./output.pdf\");\n```\n\nAll render functions accept an optional second argument with:\n\n- `registry` - Custom component registry (merged with standard components)\n- `state` - Initial state for `$state` / `$cond` dynamic prop resolution\n- `handlers` - Action handlers\n\n## External Store (Controlled Mode)\n\nFor full control over state, pass a `StateStore` to `StateProvider`, `JSONUIProvider`, or `createRenderer`. When `store` is provided, `initialState` and `onStateChange` are ignored and the store is the single source of truth:\n\n```tsx\nimport { createStateStore, type StateStore } from \"@json-render/react-pdf\";\n\nconst store = createStateStore({ invoice: { total: 100 } });\n\n// Mutate from anywhere — components re-render automatically:\nstore.set(\"/invoice/total\", 200);\n```\n\n## Server-Safe Import\n\nImport schema and catalog definitions without pulling in React:\n\n```typescript\nimport { schema, standardComponentDefinitions } from \"@json-render/react-pdf/server\";\n```\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/react-pdf/package.json",
    "content": "{\n  \"name\": \"@json-render/react-pdf\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"React PDF renderer for @json-render/core. JSON becomes PDF documents.\",\n  \"keywords\": [\n    \"json\",\n    \"pdf\",\n    \"react-pdf\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"documents\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/react-pdf\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.mjs\",\n      \"require\": \"./dist/server.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    },\n    \"./render\": {\n      \"types\": \"./dist/render.d.ts\",\n      \"import\": \"./dist/render.mjs\",\n      \"require\": \"./dist/render.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@react-pdf/renderer\": \"^4.3.2\"\n  },\n  \"devDependencies\": {\n    \"@internal/react-state\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^18.0.0 || ^19.0.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-pdf/src/catalog-types.ts",
    "content": "import type { ReactNode } from \"react\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferComponentProps,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> {\n  props: InferComponentProps<C, K>;\n  children?: ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => ReactNode;\n\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n"
  },
  {
    "path": "packages/react-pdf/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Standard component definitions for React PDF catalogs.\n *\n * These define the available PDF components with their Zod prop schemas.\n * All components render using @react-pdf/renderer primitives.\n */\nexport const standardComponentDefinitions = {\n  // ==========================================================================\n  // Document Structure\n  // ==========================================================================\n\n  Document: {\n    props: z.object({\n      title: z.string().nullable(),\n      author: z.string().nullable(),\n      subject: z.string().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Top-level PDF document wrapper. Must be the root element. Children must be Page components.\",\n    example: { title: \"Invoice #1234\", author: \"Acme Corp\" },\n  },\n\n  Page: {\n    props: z.object({\n      size: z.enum([\"A4\", \"A3\", \"A5\", \"LETTER\", \"LEGAL\", \"TABLOID\"]).nullable(),\n      orientation: z.enum([\"portrait\", \"landscape\"]).nullable(),\n      marginTop: z.number().nullable(),\n      marginBottom: z.number().nullable(),\n      marginLeft: z.number().nullable(),\n      marginRight: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"A page in the PDF document. Set size and orientation. Children are laid out vertically by default.\",\n    example: { size: \"A4\", orientation: \"portrait\" },\n  },\n\n  // ==========================================================================\n  // Layout Components\n  // ==========================================================================\n\n  View: {\n    props: z.object({\n      padding: z.number().nullable(),\n      paddingTop: z.number().nullable(),\n      paddingBottom: z.number().nullable(),\n      paddingLeft: z.number().nullable(),\n      paddingRight: z.number().nullable(),\n      margin: z.number().nullable(),\n      backgroundColor: z.string().nullable(),\n      borderWidth: z.number().nullable(),\n      borderColor: z.string().nullable(),\n      borderRadius: z.number().nullable(),\n      flex: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Generic container for grouping elements. Supports padding, margin, background, border, and flex alignment.\",\n    example: { padding: 10, backgroundColor: \"#f9f9f9\", alignItems: \"center\" },\n  },\n\n  Row: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n      wrap: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Horizontal flex layout. Use for placing elements side by side.\",\n    example: { gap: 10, alignItems: \"center\" },\n  },\n\n  Column: {\n    props: z.object({\n      gap: z.number().nullable(),\n      alignItems: z\n        .enum([\"flex-start\", \"center\", \"flex-end\", \"stretch\"])\n        .nullable(),\n      justifyContent: z\n        .enum([\n          \"flex-start\",\n          \"center\",\n          \"flex-end\",\n          \"space-between\",\n          \"space-around\",\n        ])\n        .nullable(),\n      padding: z.number().nullable(),\n      flex: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Vertical flex layout. Use for stacking elements top to bottom.\",\n    example: { gap: 8, padding: 10 },\n  },\n\n  // ==========================================================================\n  // Content Components\n  // ==========================================================================\n\n  Heading: {\n    props: z.object({\n      text: z.string(),\n      level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Heading text at various levels. h1 is largest, h4 is smallest.\",\n    example: { text: \"Invoice\", level: \"h1\" },\n  },\n\n  Text: {\n    props: z.object({\n      text: z.string(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n      fontWeight: z.enum([\"normal\", \"bold\"]).nullable(),\n      fontStyle: z.enum([\"normal\", \"italic\"]).nullable(),\n      lineHeight: z.number().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Body text with configurable size, color, weight, and alignment.\",\n    example: { text: \"Thank you for your business.\" },\n  },\n\n  Image: {\n    props: z.object({\n      src: z.string(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      objectFit: z.enum([\"contain\", \"cover\", \"fill\", \"none\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      \"Image from a URL. Specify width and/or height to control size. For placeholder/stock images use https://picsum.photos/{width}/{height}?random={n} where {n} is a unique number per image.\",\n    example: {\n      src: \"https://picsum.photos/400/300?random=1\",\n      width: 400,\n      height: 300,\n    },\n  },\n\n  Link: {\n    props: z.object({\n      text: z.string(),\n      href: z.string(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n    }),\n    slots: [],\n    description: \"Hyperlink with visible text and a URL.\",\n    example: { text: \"Visit our website\", href: \"https://example.com\" },\n  },\n\n  // ==========================================================================\n  // Data Components\n  // ==========================================================================\n\n  Table: {\n    props: z.object({\n      columns: z.array(\n        z.object({\n          header: z.string(),\n          width: z.string().nullable(),\n          align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n        }),\n      ),\n      rows: z.array(z.array(z.string())),\n      headerBackgroundColor: z.string().nullable(),\n      headerTextColor: z.string().nullable(),\n      borderColor: z.string().nullable(),\n      fontSize: z.number().nullable(),\n      striped: z.boolean().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Data table with typed columns and rows. Each row is a string array matching the column count.\",\n    example: {\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  },\n\n  List: {\n    props: z.object({\n      items: z.array(z.string()),\n      ordered: z.boolean().nullable(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n      spacing: z.number().nullable(),\n    }),\n    slots: [],\n    description:\n      \"Ordered or unordered list. Each item is rendered as a text line with a bullet or number.\",\n    example: {\n      items: [\"First item\", \"Second item\", \"Third item\"],\n      ordered: false,\n    },\n  },\n\n  // ==========================================================================\n  // Decorative Components\n  // ==========================================================================\n\n  Divider: {\n    props: z.object({\n      color: z.string().nullable(),\n      thickness: z.number().nullable(),\n      marginTop: z.number().nullable(),\n      marginBottom: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Horizontal line separator between content sections.\",\n    example: { color: \"#e5e7eb\", thickness: 1 },\n  },\n\n  Spacer: {\n    props: z.object({\n      height: z.number().nullable(),\n    }),\n    slots: [],\n    description: \"Empty vertical space between elements.\",\n    example: { height: 20 },\n  },\n\n  // ==========================================================================\n  // Page-Level Components\n  // ==========================================================================\n\n  PageNumber: {\n    props: z.object({\n      format: z.string().nullable(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n      align: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n    }),\n    slots: [],\n    description:\n      'Renders the current page number and total pages. Format uses {pageNumber} and {totalPages} placeholders, e.g. \"Page {pageNumber} of {totalPages}\". Default: \"{pageNumber} / {totalPages}\".',\n    example: {\n      format: \"Page {pageNumber} of {totalPages}\",\n      align: \"center\",\n      fontSize: 10,\n    },\n  },\n};\n\nexport type StandardComponentDefinitions = typeof standardComponentDefinitions;\n\nexport type StandardComponentProps<\n  K extends keyof StandardComponentDefinitions,\n> = StandardComponentDefinitions[K][\"props\"] extends { _output: infer O }\n  ? O\n  : z.output<StandardComponentDefinitions[K][\"props\"]>;\n"
  },
  {
    "path": "packages/react-pdf/src/components/index.ts",
    "content": "export { standardComponents } from \"./standard\";\n"
  },
  {
    "path": "packages/react-pdf/src/components/standard.tsx",
    "content": "import React from \"react\";\nimport {\n  Document as PdfDocument,\n  Page as PdfPage,\n  View,\n  Text as PdfText,\n  Image as PdfImage,\n  Link as PdfLink,\n  StyleSheet,\n} from \"@react-pdf/renderer\";\nimport type { ComponentRenderProps } from \"../renderer\";\nimport type { ComponentRegistry } from \"../renderer\";\nimport type { StandardComponentProps } from \"../catalog\";\n\nconst EMOJI_RE =\n  /[\\u{1F600}-\\u{1F64F}\\u{1F300}-\\u{1F5FF}\\u{1F680}-\\u{1F6FF}\\u{1F1E0}-\\u{1F1FF}\\u{2600}-\\u{26FF}\\u{2700}-\\u{27BF}\\u{FE00}-\\u{FE0F}\\u{1F900}-\\u{1F9FF}\\u{1FA00}-\\u{1FA6F}\\u{1FA70}-\\u{1FAFF}\\u{200D}\\u{20E3}\\u{E0020}-\\u{E007F}]/gu;\n\nfunction stripEmoji(text: string): string {\n  return text\n    .replace(EMOJI_RE, \"\")\n    .replace(/\\s{2,}/g, \" \")\n    .trim();\n}\n\n// =============================================================================\n// Document Structure\n// =============================================================================\n\nfunction DocumentComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Document\">>) {\n  const p = element.props;\n\n  return (\n    <PdfDocument\n      title={p.title ?? undefined}\n      author={p.author ?? undefined}\n      subject={p.subject ?? undefined}\n    >\n      {children}\n    </PdfDocument>\n  );\n}\n\nfunction PageComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Page\">>) {\n  const p = element.props;\n\n  return (\n    <PdfPage\n      size={(p.size as any) ?? \"A4\"}\n      orientation={p.orientation ?? \"portrait\"}\n      style={{\n        paddingTop: p.marginTop ?? 40,\n        paddingBottom: p.marginBottom ?? 40,\n        paddingLeft: p.marginLeft ?? 40,\n        paddingRight: p.marginRight ?? 40,\n        backgroundColor: p.backgroundColor ?? undefined,\n        fontFamily: \"Helvetica\",\n        fontSize: 12,\n      }}\n    >\n      {children}\n    </PdfPage>\n  );\n}\n\n// =============================================================================\n// Layout Components\n// =============================================================================\n\nfunction ViewComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"View\">>) {\n  const p = element.props;\n\n  return (\n    <View\n      style={{\n        padding: p.padding ?? undefined,\n        paddingTop: p.paddingTop ?? undefined,\n        paddingBottom: p.paddingBottom ?? undefined,\n        paddingLeft: p.paddingLeft ?? undefined,\n        paddingRight: p.paddingRight ?? undefined,\n        margin: p.margin ?? undefined,\n        backgroundColor: p.backgroundColor ?? undefined,\n        borderWidth: p.borderWidth ?? undefined,\n        borderColor: p.borderColor ?? undefined,\n        borderRadius: p.borderRadius ?? undefined,\n        flex: p.flex ?? undefined,\n        alignItems: (p.alignItems as any) ?? undefined,\n        justifyContent: (p.justifyContent as any) ?? undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\nfunction RowComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Row\">>) {\n  const p = element.props;\n\n  return (\n    <View\n      style={{\n        flexDirection: \"row\",\n        gap: p.gap ?? undefined,\n        alignItems: (p.alignItems as any) ?? undefined,\n        justifyContent: (p.justifyContent as any) ?? undefined,\n        padding: p.padding ?? undefined,\n        flex: p.flex ?? undefined,\n        flexWrap: p.wrap ? \"wrap\" : undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\nfunction ColumnComponent({\n  element,\n  children,\n}: ComponentRenderProps<StandardComponentProps<\"Column\">>) {\n  const p = element.props;\n\n  return (\n    <View\n      style={{\n        flexDirection: \"column\",\n        gap: p.gap ?? undefined,\n        alignItems: (p.alignItems as any) ?? undefined,\n        justifyContent: (p.justifyContent as any) ?? undefined,\n        padding: p.padding ?? undefined,\n        flex: p.flex ?? undefined,\n      }}\n    >\n      {children}\n    </View>\n  );\n}\n\n// =============================================================================\n// Content Components\n// =============================================================================\n\nconst headingStyles = StyleSheet.create({\n  h1: { fontSize: 24, fontFamily: \"Helvetica-Bold\", marginBottom: 8 },\n  h2: { fontSize: 20, fontFamily: \"Helvetica-Bold\", marginBottom: 6 },\n  h3: { fontSize: 16, fontFamily: \"Helvetica-Bold\", marginBottom: 4 },\n  h4: { fontSize: 14, fontFamily: \"Helvetica-Bold\", marginBottom: 4 },\n});\n\nfunction HeadingComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Heading\">>) {\n  const p = element.props;\n  const level = p.level ?? \"h2\";\n\n  return (\n    <PdfText\n      style={[\n        headingStyles[level],\n        {\n          color: p.color ?? undefined,\n          textAlign: p.align ?? \"left\",\n        },\n      ]}\n    >\n      {stripEmoji(p.text)}\n    </PdfText>\n  );\n}\n\nfunction TextComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Text\">>) {\n  const p = element.props;\n\n  return (\n    <PdfText\n      style={{\n        fontSize: p.fontSize ?? 12,\n        color: p.color ?? undefined,\n        textAlign: p.align ?? \"left\",\n        fontFamily:\n          p.fontWeight === \"bold\" && p.fontStyle === \"italic\"\n            ? \"Helvetica-BoldOblique\"\n            : p.fontWeight === \"bold\"\n              ? \"Helvetica-Bold\"\n              : p.fontStyle === \"italic\"\n                ? \"Helvetica-Oblique\"\n                : \"Helvetica\",\n        lineHeight: p.lineHeight ?? undefined,\n      }}\n    >\n      {stripEmoji(p.text)}\n    </PdfText>\n  );\n}\n\nfunction ImageComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Image\">>) {\n  const p = element.props;\n\n  return (\n    <PdfImage\n      src={p.src}\n      style={{\n        width: p.width ?? undefined,\n        height: p.height ?? undefined,\n        objectFit: p.objectFit ?? \"contain\",\n      }}\n    />\n  );\n}\n\nfunction LinkComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Link\">>) {\n  const p = element.props;\n\n  return (\n    <PdfLink\n      src={p.href}\n      style={{\n        fontSize: p.fontSize ?? 12,\n        color: p.color ?? \"#2563eb\",\n        textDecoration: \"underline\",\n      }}\n    >\n      {stripEmoji(p.text)}\n    </PdfLink>\n  );\n}\n\n// =============================================================================\n// Data Components\n// =============================================================================\n\nconst tableStyles = StyleSheet.create({\n  table: {\n    width: \"100%\",\n  },\n  row: {\n    flexDirection: \"row\",\n  },\n  cell: {\n    padding: 6,\n  },\n  headerCell: {\n    padding: 6,\n    fontFamily: \"Helvetica-Bold\",\n  },\n  bottomBorder: {\n    borderBottomWidth: 1,\n  },\n});\n\nfunction TableComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Table\">>) {\n  const p = element.props;\n\n  const borderColor = p.borderColor ?? \"#e5e7eb\";\n  const fontSize = p.fontSize ?? 10;\n  const defaultWidth = `${100 / p.columns.length}%`;\n\n  return (\n    <View style={tableStyles.table}>\n      {/* Header */}\n      <View\n        style={[\n          tableStyles.row,\n          tableStyles.bottomBorder,\n          {\n            borderBottomColor: borderColor,\n            backgroundColor: p.headerBackgroundColor ?? \"#f3f4f6\",\n          },\n        ]}\n      >\n        {p.columns.map((col, i) => (\n          <View\n            key={i}\n            style={[\n              tableStyles.headerCell,\n              { width: col.width ?? defaultWidth },\n            ]}\n          >\n            <PdfText\n              style={{\n                fontSize,\n                color: p.headerTextColor ?? \"#111827\",\n                textAlign: col.align ?? \"left\",\n              }}\n            >\n              {stripEmoji(col.header)}\n            </PdfText>\n          </View>\n        ))}\n      </View>\n\n      {/* Rows */}\n      {p.rows.map((row, rowIndex) => (\n        <View\n          key={rowIndex}\n          style={[\n            tableStyles.row,\n            tableStyles.bottomBorder,\n            {\n              borderBottomColor: borderColor,\n              backgroundColor:\n                p.striped && rowIndex % 2 === 1 ? \"#f9fafb\" : undefined,\n            },\n          ]}\n        >\n          {p.columns.map((col, colIndex) => (\n            <View\n              key={colIndex}\n              style={[tableStyles.cell, { width: col.width ?? defaultWidth }]}\n            >\n              <PdfText\n                style={{\n                  fontSize,\n                  textAlign: col.align ?? \"left\",\n                }}\n              >\n                {stripEmoji(row[colIndex] ?? \"\")}\n              </PdfText>\n            </View>\n          ))}\n        </View>\n      ))}\n    </View>\n  );\n}\n\nfunction ListComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"List\">>) {\n  const p = element.props;\n\n  const fontSize = p.fontSize ?? 12;\n  const spacing = p.spacing ?? 4;\n\n  return (\n    <View style={{ gap: spacing }}>\n      {p.items.map((item, index) => (\n        <View key={index} style={{ flexDirection: \"row\", gap: 6 }}>\n          <PdfText\n            style={{\n              fontSize,\n              color: p.color ?? undefined,\n              width: p.ordered ? 20 : 12,\n            }}\n          >\n            {p.ordered ? `${index + 1}.` : \"\\u2022\"}\n          </PdfText>\n          <PdfText\n            style={{\n              fontSize,\n              color: p.color ?? undefined,\n              flex: 1,\n            }}\n          >\n            {stripEmoji(item)}\n          </PdfText>\n        </View>\n      ))}\n    </View>\n  );\n}\n\n// =============================================================================\n// Decorative Components\n// =============================================================================\n\nfunction DividerComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Divider\">>) {\n  const p = element.props;\n\n  return (\n    <View\n      style={{\n        borderBottomWidth: p.thickness ?? 1,\n        borderBottomColor: p.color ?? \"#e5e7eb\",\n        marginTop: p.marginTop ?? 8,\n        marginBottom: p.marginBottom ?? 8,\n      }}\n    />\n  );\n}\n\nfunction SpacerComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"Spacer\">>) {\n  const p = element.props;\n\n  return <View style={{ height: p.height ?? 20 }} />;\n}\n\n// =============================================================================\n// Page-Level Components\n// =============================================================================\n\nfunction PageNumberComponent({\n  element,\n}: ComponentRenderProps<StandardComponentProps<\"PageNumber\">>) {\n  const p = element.props;\n\n  const format = p.format ?? \"{pageNumber} / {totalPages}\";\n\n  return (\n    <PdfText\n      style={{\n        fontSize: p.fontSize ?? 10,\n        color: p.color ?? \"#6b7280\",\n        textAlign: p.align ?? \"center\",\n      }}\n      render={({ pageNumber, totalPages }) =>\n        stripEmoji(\n          format\n            .replace(\"{pageNumber}\", String(pageNumber))\n            .replace(\"{totalPages}\", String(totalPages)),\n        )\n      }\n      fixed\n    />\n  );\n}\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport const standardComponents: ComponentRegistry = {\n  Document: DocumentComponent,\n  Page: PageComponent,\n  View: ViewComponent,\n  Row: RowComponent,\n  Column: ColumnComponent,\n  Heading: HeadingComponent,\n  Text: TextComponent,\n  Image: ImageComponent,\n  Link: LinkComponent,\n  Table: TableComponent,\n  List: ListComponent,\n  Divider: DividerComponent,\n  Spacer: SpacerComponent,\n  PageNumber: PageNumberComponent,\n};\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/actions.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nlet idCounter = 0;\nfunction generateUniqueId(): string {\n  idCounter += 1;\n  return `${Date.now()}-${idCounter}`;\n}\n\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\nexport interface PendingConfirmation {\n  action: ResolvedAction;\n  handler: ActionHandler;\n  resolve: () => void;\n  reject: () => void;\n}\n\nexport interface ActionContextValue {\n  handlers: Record<string, ActionHandler>;\n  loadingActions: Set<string>;\n  pendingConfirmation: PendingConfirmation | null;\n  execute: (binding: ActionBinding) => Promise<void>;\n  confirm: () => void;\n  cancel: () => void;\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ActionContext = createContext<ActionContextValue | null>(null);\n\nexport interface ActionProviderProps {\n  handlers?: Record<string, ActionHandler>;\n  navigate?: (path: string) => void;\n  children: ReactNode;\n}\n\nexport function ActionProvider({\n  handlers: initialHandlers = {},\n  navigate,\n  children,\n}: ActionProviderProps) {\n  const { get, set, getSnapshot } = useStateStore();\n  const [handlers, setHandlers] =\n    useState<Record<string, ActionHandler>>(initialHandlers);\n  const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());\n  const [pendingConfirmation, setPendingConfirmation] =\n    useState<PendingConfirmation | null>(null);\n\n  const registerHandler = useCallback(\n    (name: string, handler: ActionHandler) => {\n      setHandlers((prev) => ({ ...prev, [name]: handler }));\n    },\n    [],\n  );\n\n  const execute = useCallback(\n    async (binding: ActionBinding) => {\n      const resolved = resolveAction(binding, getSnapshot());\n\n      if (resolved.action === \"setState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const value = resolved.params.value;\n        if (statePath) {\n          set(statePath, value);\n        }\n        return;\n      }\n\n      if (resolved.action === \"pushState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const rawValue = resolved.params.value;\n        if (statePath) {\n          const resolvedValue = deepResolveValue(rawValue, get);\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(statePath, [...arr, resolvedValue]);\n          const clearStatePath = resolved.params.clearStatePath as\n            | string\n            | undefined;\n          if (clearStatePath) {\n            set(clearStatePath, \"\");\n          }\n        }\n        return;\n      }\n\n      if (resolved.action === \"removeState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const index = resolved.params.index as number;\n        if (statePath !== undefined && index !== undefined) {\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(\n            statePath,\n            arr.filter((_, i) => i !== index),\n          );\n        }\n        return;\n      }\n\n      const handler = handlers[resolved.action];\n\n      if (!handler) {\n        console.warn(`No handler registered for action: ${resolved.action}`);\n        return;\n      }\n\n      if (resolved.confirm) {\n        return new Promise<void>((resolve, reject) => {\n          setPendingConfirmation({\n            action: resolved,\n            handler,\n            resolve: () => {\n              setPendingConfirmation(null);\n              resolve();\n            },\n            reject: () => {\n              setPendingConfirmation(null);\n              reject(new Error(\"Action cancelled\"));\n            },\n          });\n        }).then(async () => {\n          setLoadingActions((prev) => new Set(prev).add(resolved.action));\n          try {\n            await executeAction({\n              action: resolved,\n              handler,\n              setState: set,\n              navigate,\n              executeAction: async (name) => {\n                const subBinding: ActionBinding = { action: name };\n                await execute(subBinding);\n              },\n            });\n          } finally {\n            setLoadingActions((prev) => {\n              const next = new Set(prev);\n              next.delete(resolved.action);\n              return next;\n            });\n          }\n        });\n      }\n\n      setLoadingActions((prev) => new Set(prev).add(resolved.action));\n      try {\n        await executeAction({\n          action: resolved,\n          handler,\n          setState: set,\n          navigate,\n          executeAction: async (name) => {\n            const subBinding: ActionBinding = { action: name };\n            await execute(subBinding);\n          },\n        });\n      } finally {\n        setLoadingActions((prev) => {\n          const next = new Set(prev);\n          next.delete(resolved.action);\n          return next;\n        });\n      }\n    },\n    [handlers, get, set, getSnapshot, navigate],\n  );\n\n  const confirm = useCallback(() => {\n    pendingConfirmation?.resolve();\n  }, [pendingConfirmation]);\n\n  const cancel = useCallback(() => {\n    pendingConfirmation?.reject();\n  }, [pendingConfirmation]);\n\n  const value = useMemo<ActionContextValue>(\n    () => ({\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    }),\n    [\n      handlers,\n      loadingActions,\n      pendingConfirmation,\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    ],\n  );\n\n  return (\n    <ActionContext.Provider value={value}>{children}</ActionContext.Provider>\n  );\n}\n\nexport function useActions(): ActionContextValue {\n  const ctx = useContext(ActionContext);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: boolean;\n} {\n  const { execute, loadingActions } = useActions();\n  const isLoading = loadingActions.has(binding.action);\n\n  const executeAction = useCallback(() => execute(binding), [execute, binding]);\n\n  return { execute: executeAction, isLoading };\n}\n\nexport interface ConfirmDialogProps {\n  confirm: ActionConfirm;\n  onConfirm: () => void;\n  onCancel: () => void;\n}\n\n/**\n * No-op confirm dialog for PDF context. PDFs are non-interactive,\n * so confirmations are not rendered.\n */\nexport function ConfirmDialog(_props: ConfirmDialogProps) {\n  return null;\n}\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/repeat-scope.tsx",
    "content": "import React, { createContext, useContext, type ReactNode } from \"react\";\n\nexport interface RepeatScopeValue {\n  item: unknown;\n  index: number;\n  basePath: string;\n}\n\nconst RepeatScopeContext = createContext<RepeatScopeValue | null>(null);\n\nexport function RepeatScopeProvider({\n  item,\n  index,\n  basePath,\n  children,\n}: RepeatScopeValue & { children: ReactNode }) {\n  return (\n    <RepeatScopeContext.Provider value={{ item, index, basePath }}>\n      {children}\n    </RepeatScopeContext.Provider>\n  );\n}\n\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return useContext(RepeatScopeContext);\n}\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/state.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport React from \"react\";\nimport { renderHook, act } from \"@testing-library/react\";\nimport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n} from \"./state\";\n\ndescribe(\"state re-exports (smoke test)\", () => {\n  it(\"StateProvider + useStateStore round-trip\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ count: 0 }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.get(\"/count\")).toBe(0);\n\n    act(() => {\n      result.current.set(\"/count\", 42);\n    });\n\n    expect(result.current.state.count).toBe(42);\n  });\n\n  it(\"useStateValue reads from state\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ name: \"Alice\" }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateValue(\"/name\"), { wrapper });\n\n    expect(result.current).toBe(\"Alice\");\n  });\n\n  it(\"useStateBinding returns value and setter\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ x: 1 }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateBinding(\"/x\"), { wrapper });\n\n    const [value, setValue] = result.current;\n    expect(value).toBe(1);\n    expect(typeof setValue).toBe(\"function\");\n  });\n});\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/state.tsx",
    "content": "export {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"@internal/react-state\";\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/validation.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface FieldValidationState {\n  touched: boolean;\n  validated: boolean;\n  result: ValidationResult | null;\n}\n\nexport interface ValidationContextValue {\n  customFunctions: Record<string, ValidationFunction>;\n  fieldStates: Record<string, FieldValidationState>;\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  touch: (path: string) => void;\n  clear: (path: string) => void;\n  validateAll: () => boolean;\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst ValidationContext = createContext<ValidationContextValue | null>(null);\n\nexport interface ValidationProviderProps {\n  customFunctions?: Record<string, ValidationFunction>;\n  children: ReactNode;\n}\n\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n  if (a.validateOn !== b.validateOn) return false;\n\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n\n  return true;\n}\n\nexport function ValidationProvider({\n  customFunctions = {},\n  children,\n}: ValidationProviderProps) {\n  const { state } = useStateStore();\n  const [fieldStates, setFieldStates] = useState<\n    Record<string, FieldValidationState>\n  >({});\n  const [fieldConfigs, setFieldConfigs] = useState<\n    Record<string, ValidationConfig>\n  >({});\n\n  const registerField = useCallback(\n    (path: string, config: ValidationConfig) => {\n      setFieldConfigs((prev) => {\n        const existing = prev[path];\n        if (existing && validationConfigEqual(existing, config)) {\n          return prev;\n        }\n        return { ...prev, [path]: config };\n      });\n    },\n    [],\n  );\n\n  const validate = useCallback(\n    (path: string, config: ValidationConfig): ValidationResult => {\n      const segments = path.split(\"/\").filter(Boolean);\n      let value: unknown = state;\n      for (const seg of segments) {\n        if (value != null && typeof value === \"object\") {\n          value = (value as Record<string, unknown>)[seg];\n        } else {\n          value = undefined;\n          break;\n        }\n      }\n      const result = runValidation(config, {\n        value,\n        stateModel: state,\n        customFunctions,\n      });\n\n      setFieldStates((prev) => ({\n        ...prev,\n        [path]: {\n          touched: prev[path]?.touched ?? true,\n          validated: true,\n          result,\n        },\n      }));\n\n      return result;\n    },\n    [state, customFunctions],\n  );\n\n  const touch = useCallback((path: string) => {\n    setFieldStates((prev) => ({\n      ...prev,\n      [path]: {\n        ...prev[path],\n        touched: true,\n        validated: prev[path]?.validated ?? false,\n        result: prev[path]?.result ?? null,\n      },\n    }));\n  }, []);\n\n  const clear = useCallback((path: string) => {\n    setFieldStates((prev) => {\n      const { [path]: _, ...rest } = prev;\n      return rest;\n    });\n  }, []);\n\n  const validateAll = useCallback(() => {\n    let allValid = true;\n    for (const [path, config] of Object.entries(fieldConfigs)) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n    return allValid;\n  }, [fieldConfigs, validate]);\n\n  const value = useMemo<ValidationContextValue>(\n    () => ({\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    }),\n    [\n      customFunctions,\n      fieldStates,\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    ],\n  );\n\n  return (\n    <ValidationContext.Provider value={value}>\n      {children}\n    </ValidationContext.Provider>\n  );\n}\n\nexport function useValidation(): ValidationContextValue {\n  const ctx = useContext(ValidationContext);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: FieldValidationState;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: string[];\n  isValid: boolean;\n} {\n  const {\n    fieldStates,\n    validate: validateField,\n    touch: touchField,\n    clear: clearField,\n    registerField,\n  } = useValidation();\n\n  React.useEffect(() => {\n    if (config) {\n      registerField(path, config);\n    }\n  }, [path, config, registerField]);\n\n  const state = fieldStates[path] ?? {\n    touched: false,\n    validated: false,\n    result: null,\n  };\n\n  const validate = useCallback(\n    () => validateField(path, config ?? { checks: [] }),\n    [path, config, validateField],\n  );\n\n  const touch = useCallback(() => touchField(path), [path, touchField]);\n  const clear = useCallback(() => clearField(path), [path, clearField]);\n\n  return {\n    state,\n    validate,\n    touch,\n    clear,\n    errors: state.result?.errors ?? [],\n    isValid: state.result?.valid ?? true,\n  };\n}\n"
  },
  {
    "path": "packages/react-pdf/src/contexts/visibility.tsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface VisibilityContextValue {\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  ctx: CoreVisibilityContext;\n}\n\nconst VisibilityContext = createContext<VisibilityContextValue | null>(null);\n\nexport interface VisibilityProviderProps {\n  children: ReactNode;\n}\n\nexport function VisibilityProvider({ children }: VisibilityProviderProps) {\n  const { state } = useStateStore();\n\n  const ctx: CoreVisibilityContext = useMemo(\n    () => ({ stateModel: state }),\n    [state],\n  );\n\n  const isVisible = useMemo(\n    () => (condition: VisibilityCondition | undefined) =>\n      evaluateVisibility(condition, ctx),\n    [ctx],\n  );\n\n  const value = useMemo<VisibilityContextValue>(\n    () => ({ isVisible, ctx }),\n    [isVisible, ctx],\n  );\n\n  return (\n    <VisibilityContext.Provider value={value}>\n      {children}\n    </VisibilityContext.Provider>\n  );\n}\n\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = useContext(VisibilityContext);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): boolean {\n  const { isVisible } = useVisibility();\n  return isVisible(condition);\n}\n"
  },
  {
    "path": "packages/react-pdf/src/index.ts",
    "content": "// Schema\nexport { schema, type ReactPdfSchema, type ReactPdfSpec } from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec, StateStore } from \"@json-render/core\";\nexport { createStateStore } from \"@json-render/core\";\n\n// Catalog-aware types\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n\n// Contexts\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./contexts/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n  type VisibilityProviderProps,\n} from \"./contexts/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./contexts/actions\";\n\nexport {\n  ValidationProvider,\n  useValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./contexts/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/repeat-scope\";\n\n// Renderer\nexport {\n  defineRegistry,\n  type DefineRegistryResult,\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRenderer,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n} from \"./renderer\";\n\n// Standard components\nexport { standardComponents } from \"./components\";\n\n// Server-side render functions\nexport {\n  renderToBuffer,\n  renderToStream,\n  renderToFile,\n  type RenderOptions,\n} from \"./render\";\n\n// Catalog definitions\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/react-pdf/src/render.tsx",
    "content": "import React from \"react\";\nimport {\n  renderToBuffer as pdfRenderToBuffer,\n  renderToStream as pdfRenderToStream,\n  render as pdfRender,\n} from \"@react-pdf/renderer\";\nimport type { Spec, UIElement } from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n} from \"@json-render/core\";\nimport { standardComponents } from \"./components/standard\";\n\n// Re-export the standard components for use in custom registries\nexport { standardComponents };\n\nexport type RenderComponentRegistry = Record<string, React.ComponentType<any>>;\n\nexport interface RenderOptions {\n  registry?: RenderComponentRegistry;\n  includeStandard?: boolean;\n  state?: Record<string, unknown>;\n}\n\nconst noopEmit = () => {};\n\nfunction renderElement(\n  elementKey: string,\n  spec: Spec,\n  registry: RenderComponentRegistry,\n  stateModel: Record<string, unknown>,\n  repeatItem?: unknown,\n  repeatIndex?: number,\n  repeatBasePath?: string,\n): React.ReactElement | null {\n  const element = spec.elements[elementKey];\n  if (!element) return null;\n\n  const ctx: PropResolutionContext = {\n    stateModel,\n    repeatItem,\n    repeatIndex,\n    repeatBasePath,\n  };\n\n  if (element.visible !== undefined) {\n    if (!evaluateVisibility(element.visible, ctx)) {\n      return null;\n    }\n  }\n\n  const resolvedProps = resolveElementProps(\n    element.props as Record<string, unknown>,\n    ctx,\n  );\n  const resolvedElement: UIElement = { ...element, props: resolvedProps };\n\n  const Component = registry[resolvedElement.type];\n  if (!Component) return null;\n\n  if (resolvedElement.repeat) {\n    const items =\n      (getByPath(stateModel, resolvedElement.repeat.statePath) as\n        | unknown[]\n        | undefined) ?? [];\n\n    const fragments = items.map((item, index) => {\n      const key =\n        resolvedElement.repeat!.key && typeof item === \"object\" && item !== null\n          ? String(\n              (item as Record<string, unknown>)[resolvedElement.repeat!.key!] ??\n                index,\n            )\n          : String(index);\n\n      const childPath = `${resolvedElement.repeat!.statePath}/${index}`;\n      const children = resolvedElement.children?.map((childKey) =>\n        renderElement(\n          childKey,\n          spec,\n          registry,\n          stateModel,\n          item,\n          index,\n          childPath,\n        ),\n      );\n\n      return (\n        <Component key={key} element={resolvedElement} emit={noopEmit}>\n          {children}\n        </Component>\n      );\n    });\n\n    return <>{fragments}</>;\n  }\n\n  const children = resolvedElement.children?.map((childKey) =>\n    renderElement(\n      childKey,\n      spec,\n      registry,\n      stateModel,\n      repeatItem,\n      repeatIndex,\n      repeatBasePath,\n    ),\n  );\n\n  return (\n    <Component key={elementKey} element={resolvedElement} emit={noopEmit}>\n      {children && children.length > 0 ? children : undefined}\n    </Component>\n  );\n}\n\nfunction buildDocument(\n  spec: Spec,\n  options: RenderOptions = {},\n): React.ReactElement {\n  const {\n    registry: customRegistry,\n    includeStandard = true,\n    state = {},\n  } = options;\n\n  const mergedState: Record<string, unknown> = {\n    ...spec.state,\n    ...state,\n  };\n\n  const registry: RenderComponentRegistry = {\n    ...(includeStandard ? standardComponents : {}),\n    ...customRegistry,\n  };\n\n  const root = renderElement(spec.root, spec, registry, mergedState);\n  return root ?? <></>;\n}\n\n/**\n * Render a json-render spec to a PDF buffer.\n *\n * This is a standalone server-side function that resolves the spec tree\n * without React hooks or contexts, making it safe to import in Next.js\n * route handlers and other server-only environments.\n */\nexport async function renderToBuffer(\n  spec: Spec,\n  options?: RenderOptions,\n): Promise<Uint8Array> {\n  const document = buildDocument(spec, options);\n  return pdfRenderToBuffer(document as any);\n}\n\n/**\n * Render a json-render spec to a PDF readable stream.\n */\nexport async function renderToStream(\n  spec: Spec,\n  options?: RenderOptions,\n): Promise<ReadableStream> {\n  const document = buildDocument(spec, options);\n  return pdfRenderToStream(document as any);\n}\n\n/**\n * Render a json-render spec to a PDF file on disk.\n */\nexport async function renderToFile(\n  spec: Spec,\n  filePath: string,\n  options?: RenderOptions,\n): Promise<void> {\n  const document = buildDocument(spec, options);\n  await pdfRender(document as any, filePath);\n}\n"
  },
  {
    "path": "packages/react-pdf/src/renderer.tsx",
    "content": "import React, {\n  type ComponentType,\n  type ErrorInfo,\n  type ReactNode,\n  useCallback,\n  useMemo,\n} from \"react\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  SchemaDefinition,\n  StateStore,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport type { Components, SetState, StateModel } from \"./catalog-types\";\nimport { useIsVisible, useVisibility } from \"./contexts/visibility\";\nimport { useActions } from \"./contexts/actions\";\nimport { useStateStore } from \"./contexts/state\";\nimport { StateProvider } from \"./contexts/state\";\nimport { VisibilityProvider } from \"./contexts/visibility\";\nimport { ActionProvider } from \"./contexts/actions\";\nimport { ValidationProvider } from \"./contexts/validation\";\nimport { standardComponents } from \"./components/standard\";\nimport { RepeatScopeProvider, useRepeatScope } from \"./contexts/repeat-scope\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  element: UIElement<string, P>;\n  children?: ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\nexport type ComponentRenderer<P = Record<string, unknown>> = ComponentType<\n  ComponentRenderProps<P>\n>;\n\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\nexport interface RendererProps {\n  spec: Spec | null;\n  registry?: ComponentRegistry;\n  includeStandard?: boolean;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\n// =============================================================================\n// ElementErrorBoundary\n// =============================================================================\n\ninterface ElementErrorBoundaryProps {\n  elementType: string;\n  children: ReactNode;\n}\n\ninterface ElementErrorBoundaryState {\n  hasError: boolean;\n}\n\nclass ElementErrorBoundary extends React.Component<\n  ElementErrorBoundaryProps,\n  ElementErrorBoundaryState\n> {\n  constructor(props: ElementErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ElementErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, info: ErrorInfo) {\n    console.error(\n      `[json-render/react-pdf] Rendering error in <${this.props.elementType}>:`,\n      error,\n      info.componentStack,\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return null;\n    }\n    return this.props.children;\n  }\n}\n\n// =============================================================================\n// ElementRenderer\n// =============================================================================\n\ninterface ElementRendererProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\nconst ElementRenderer = React.memo(function ElementRenderer({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: ElementRendererProps) {\n  const repeatScope = useRepeatScope();\n  const { ctx } = useVisibility();\n  const { execute } = useActions();\n  const { getSnapshot } = useStateStore();\n\n  const fullCtx: PropResolutionContext = useMemo(\n    () =>\n      repeatScope\n        ? {\n            ...ctx,\n            repeatItem: repeatScope.item,\n            repeatIndex: repeatScope.index,\n            repeatBasePath: repeatScope.basePath,\n          }\n        : ctx,\n    [ctx, repeatScope],\n  );\n\n  const isVisible =\n    element.visible === undefined\n      ? true\n      : evaluateVisibility(element.visible, fullCtx);\n\n  const onBindings = element.on;\n  const emit = useCallback(\n    async (eventName: string) => {\n      const binding = onBindings?.[eventName];\n      if (!binding) return;\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      for (const b of actionBindings) {\n        if (!b.params) {\n          await execute(b);\n          continue;\n        }\n        // Build a fresh context with live store state so that $state\n        // references in later actions see mutations from earlier ones.\n        const liveCtx: PropResolutionContext = {\n          ...fullCtx,\n          stateModel: getSnapshot(),\n        };\n        const resolved: Record<string, unknown> = {};\n        for (const [key, val] of Object.entries(b.params)) {\n          resolved[key] = resolveActionParam(val, liveCtx);\n        }\n        await execute({ ...b, params: resolved });\n      }\n    },\n    [onBindings, execute, fullCtx, getSnapshot],\n  );\n\n  if (!isVisible) {\n    return null;\n  }\n\n  const rawProps = element.props as Record<string, unknown>;\n  const elementBindings = resolveBindings(rawProps, fullCtx);\n  const resolvedProps = resolveElementProps(rawProps, fullCtx);\n\n  const resolvedElement =\n    resolvedProps !== element.props\n      ? { ...element, props: resolvedProps }\n      : element;\n\n  const Component = registry[resolvedElement.type] ?? fallback;\n\n  if (!Component) {\n    console.warn(\n      `[json-render/react-pdf] No renderer for component type: ${resolvedElement.type}`,\n    );\n    return null;\n  }\n\n  const children = resolvedElement.repeat ? (\n    <RepeatChildren\n      element={resolvedElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  ) : (\n    resolvedElement.children?.map((childKey) => {\n      const childElement = spec.elements[childKey];\n      if (!childElement) {\n        if (!loading) {\n          console.warn(\n            `[json-render/react-pdf] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\".`,\n          );\n        }\n        return null;\n      }\n      return (\n        <ElementRenderer\n          key={childKey}\n          element={childElement}\n          spec={spec}\n          registry={registry}\n          loading={loading}\n          fallback={fallback}\n        />\n      );\n    })\n  );\n\n  return (\n    <ElementErrorBoundary elementType={resolvedElement.type}>\n      <Component\n        element={resolvedElement}\n        emit={emit}\n        bindings={elementBindings}\n        loading={loading}\n      >\n        {children}\n      </Component>\n    </ElementErrorBoundary>\n  );\n});\n\n// =============================================================================\n// RepeatChildren\n// =============================================================================\n\nfunction RepeatChildren({\n  element,\n  spec,\n  registry,\n  loading,\n  fallback,\n}: {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}) {\n  const { state } = useStateStore();\n  const repeat = element.repeat!;\n  const statePath = repeat.statePath;\n\n  const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];\n\n  return (\n    <>\n      {items.map((itemValue, index) => {\n        const key =\n          repeat.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String(\n                (itemValue as Record<string, unknown>)[repeat.key] ?? index,\n              )\n            : String(index);\n\n        return (\n          <RepeatScopeProvider\n            key={key}\n            item={itemValue}\n            index={index}\n            basePath={`${statePath}/${index}`}\n          >\n            {element.children?.map((childKey) => {\n              const childElement = spec.elements[childKey];\n              if (!childElement) {\n                if (!loading) {\n                  console.warn(\n                    `[json-render/react-pdf] Missing element \"${childKey}\" referenced as child of \"${element.type}\" (repeat).`,\n                  );\n                }\n                return null;\n              }\n              return (\n                <ElementRenderer\n                  key={childKey}\n                  element={childElement}\n                  spec={spec}\n                  registry={registry}\n                  loading={loading}\n                  fallback={fallback}\n                />\n              );\n            })}\n          </RepeatScopeProvider>\n        );\n      })}\n    </>\n  );\n}\n\n// =============================================================================\n// Renderer\n// =============================================================================\n\nexport function Renderer({\n  spec,\n  registry: customRegistry,\n  includeStandard = true,\n  loading,\n  fallback,\n}: RendererProps) {\n  const registry: ComponentRegistry = useMemo(\n    () => ({\n      ...(includeStandard ? standardComponents : {}),\n      ...customRegistry,\n    }),\n    [customRegistry, includeStandard],\n  );\n\n  if (!spec || !spec.root) {\n    return null;\n  }\n\n  const rootElement = spec.elements[spec.root];\n  if (!rootElement) {\n    return null;\n  }\n\n  return (\n    <ElementRenderer\n      element={rootElement}\n      spec={spec}\n      registry={registry}\n      loading={loading}\n      fallback={fallback}\n    />\n  );\n}\n\n// =============================================================================\n// JSONUIProvider\n// =============================================================================\n\nexport interface JSONUIProviderProps {\n  registry?: ComponentRegistry;\n  store?: StateStore;\n  initialState?: Record<string, unknown>;\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  navigate?: (path: string) => void;\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  children: ReactNode;\n}\n\nexport function JSONUIProvider({\n  store,\n  initialState,\n  handlers,\n  navigate,\n  validationFunctions,\n  onStateChange,\n  children,\n}: JSONUIProviderProps) {\n  return (\n    <StateProvider\n      store={store}\n      initialState={initialState}\n      onStateChange={onStateChange}\n    >\n      <VisibilityProvider>\n        <ActionProvider handlers={handlers} navigate={navigate}>\n          <ValidationProvider customFunctions={validationFunctions}>\n            {children}\n          </ValidationProvider>\n        </ActionProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n// =============================================================================\n// defineRegistry\n// =============================================================================\n\nexport interface DefineRegistryResult {\n  registry: ComponentRegistry;\n}\n\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: React.ReactNode;\n  emit: (event: string) => void;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => React.ReactNode;\n\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: {\n    components?: Components<C>;\n  },\n): DefineRegistryResult {\n  const registry: ComponentRegistry = {};\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = ({\n        element,\n        children,\n        emit,\n        bindings,\n        loading,\n      }: ComponentRenderProps) => {\n        return (componentFn as DefineRegistryComponentFn)({\n          props: element.props,\n          children,\n          emit,\n          bindings,\n          loading,\n        });\n      };\n    }\n  }\n\n  return { registry };\n}\n\n// =============================================================================\n// createRenderer\n// =============================================================================\n\nexport interface CreateRendererProps {\n  spec: Spec | null;\n  store?: StateStore;\n  state?: Record<string, unknown>;\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: ComponentType<\n    ComponentRenderProps<\n      TComponents[K][\"props\"] extends { _output: infer O }\n        ? O\n        : Record<string, unknown>\n    >\n  >;\n};\n\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): ComponentType<CreateRendererProps> {\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  return function CatalogRenderer({\n    spec,\n    store,\n    state,\n    onAction,\n    onStateChange,\n    loading,\n    fallback,\n  }: CreateRendererProps) {\n    const actionHandlers = onAction\n      ? new Proxy(\n          {} as Record<\n            string,\n            (params: Record<string, unknown>) => void | Promise<void>\n          >,\n          {\n            get: (_target, prop: string) => {\n              return (params: Record<string, unknown>) =>\n                onAction(prop, params);\n            },\n            has: () => true,\n          },\n        )\n      : undefined;\n\n    return (\n      <StateProvider\n        store={store}\n        initialState={state}\n        onStateChange={onStateChange}\n      >\n        <VisibilityProvider>\n          <ActionProvider handlers={actionHandlers}>\n            <ValidationProvider>\n              <Renderer\n                spec={spec}\n                registry={registry}\n                loading={loading}\n                fallback={fallback}\n              />\n            </ValidationProvider>\n          </ActionProvider>\n        </VisibilityProvider>\n      </StateProvider>\n    );\n  };\n}\n"
  },
  {
    "path": "packages/react-pdf/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/react-pdf\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas\n *\n * Reuses the same { root, elements } spec format as the React and React Native renderers.\n */\nexport const schema = defineSchema(\n  (s) => ({\n    spec: s.object({\n      root: s.string(),\n      elements: s.record(\n        s.object({\n          type: s.ref(\"catalog.components\"),\n          props: s.propsOf(\"catalog.components\"),\n          children: s.array(s.string()),\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    catalog: s.object({\n      components: s.map({\n        props: s.zod(),\n        slots: s.array(s.string()),\n        description: s.string(),\n        example: s.any(),\n      }),\n    }),\n  }),\n  {\n    defaultRules: [\n      \"The root element MUST be a Document component. Its children MUST be Page components.\",\n      \"Every Page must specify a size (e.g. 'A4', 'LETTER') and can set orientation, margins, and background color.\",\n      \"Use Row for horizontal layouts and Column for vertical layouts. Both support gap, align, and justify props.\",\n      \"Table columns must define header and optionally width and align. Rows is an array of string arrays matching the column count.\",\n      \"All text content must use Heading or Text components. Raw strings are not supported.\",\n      \"Image src must be a fully qualified URL. For placeholder or stock images, always use https://picsum.photos/{width}/{height}?random={n} (e.g. https://picsum.photos/400/300?random=1). Never use unsplash URLs directly.\",\n      \"PageNumber renders the current page number and total pages. Place it inside a Page.\",\n      \"NEVER use emoji characters in any text content. The PDF font (Helvetica) does not support emojis and they will render as garbled/overlapping characters. Use plain text descriptions instead (e.g. 'Phone:' not '📞', 'Email:' not '📧').\",\n      \"PAGE LAYOUT: Be conservative with content density. A portrait A4/LETTER page with 40pt margins fits roughly 700pt of content height. For single-page documents (flyers, posters, one-pagers), keep all content on one page using smaller font sizes (10-11), tighter gaps (4-8), less padding (10-15), and smaller images (max height 200). For multi-page documents (resumes, reports), pack content densely to avoid large blank areas at the bottom of pages. Use small margins (marginTop: 30, marginBottom: 20), tight spacing (gap: 4-6), and compact font sizes (9-11 for body text) so pages are well-filled. It is better to fit more content on fewer pages than to spread thin content across many pages.\",\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist.\",\n    ],\n  },\n);\n\nexport type ReactPdfSchema = typeof schema;\n\nexport type ReactPdfSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/react-pdf/src/server.ts",
    "content": "// Server-safe entry point: schema and catalog definitions only.\n// Does not import React or @react-pdf/renderer.\n\nexport { schema, type ReactPdfSchema, type ReactPdfSpec } from \"./schema\";\n\nexport {\n  standardComponentDefinitions,\n  type StandardComponentDefinitions,\n  type StandardComponentProps,\n} from \"./catalog\";\n\nexport type { Spec } from \"@json-render/core\";\n\nexport type {\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n} from \"./catalog-types\";\n"
  },
  {
    "path": "packages/react-pdf/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-pdf/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/server.ts\", \"src/catalog.ts\", \"src/render.tsx\"],\n  format: [\"cjs\", \"esm\"],\n  dts: { resolve: [\"@internal/react-state\"] },\n  sourcemap: true,\n  clean: true,\n  noExternal: [\"@internal/react-state\"],\n  external: [\"react\", \"@json-render/core\", \"@react-pdf/renderer\", \"zod\"],\n});\n"
  },
  {
    "path": "packages/react-state/CHANGELOG.md",
    "content": "# @internal/react-state\n\n## 0.8.9\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.8.8\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.8.7\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.8.6\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.8.5\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.8.4\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.8.3\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.8.2\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.8.1\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n"
  },
  {
    "path": "packages/react-state/package.json",
    "content": "{\n  \"name\": \"@internal/react-state\",\n  \"version\": \"0.8.9\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Shared React state context for json-render renderer packages\",\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^16.3.2\",\n    \"@types/react\": \"19.2.3\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^18.0.0 || ^19.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-state/src/env.d.ts",
    "content": "// Minimal process.env typing for dev-only warnings.\n// Uses a namespaced interface so it merges cleanly with @types/node if present.\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    readonly NODE_ENV?: string;\n  }\n}\n\ndeclare const process: { readonly env: NodeJS.ProcessEnv };\n"
  },
  {
    "path": "packages/react-state/src/index.test.tsx",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport React from \"react\";\nimport { renderHook, act } from \"@testing-library/react\";\nimport { createStateStore } from \"@json-render/core\";\nimport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n} from \"./index\";\n\n// ============================================================================\n// Uncontrolled mode (default)\n// ============================================================================\n\ndescribe(\"StateProvider (uncontrolled)\", () => {\n  it(\"provides initial state to children\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ user: { name: \"John\" } }}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.state).toEqual({ user: { name: \"John\" } });\n  });\n\n  it(\"provides empty object when no initial state\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.state).toEqual({});\n  });\n});\n\ndescribe(\"useStateStore (uncontrolled)\", () => {\n  it(\"provides get function to retrieve values\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ user: { name: \"John\" } }}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.get(\"/user/name\")).toBe(\"John\");\n  });\n\n  it(\"allows setting state at path with set function\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{}}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.set(\"/user/name\", \"Alice\");\n    });\n\n    expect((result.current.state.user as Record<string, unknown>).name).toBe(\n      \"Alice\",\n    );\n  });\n\n  it(\"calls onStateChange callback with changes array on set\", () => {\n    const onStateChange = vi.fn();\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{}} onStateChange={onStateChange}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.set(\"/count\", 42);\n    });\n\n    expect(onStateChange).toHaveBeenCalledTimes(1);\n    expect(onStateChange).toHaveBeenCalledWith([{ path: \"/count\", value: 42 }]);\n  });\n\n  it(\"calls onStateChange callback once with all changes on update\", () => {\n    const onStateChange = vi.fn();\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{}} onStateChange={onStateChange}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.update({ \"/name\": \"John\", \"/age\": 30 });\n    });\n\n    expect(onStateChange).toHaveBeenCalledTimes(1);\n    expect(onStateChange).toHaveBeenCalledWith([\n      { path: \"/name\", value: \"John\" },\n      { path: \"/age\", value: 30 },\n    ]);\n  });\n\n  it(\"allows updating multiple values with update function\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{}}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.update({\n        \"/name\": \"John\",\n        \"/age\": 30,\n      });\n    });\n\n    expect(result.current.state.name).toBe(\"John\");\n    expect(result.current.state.age).toBe(30);\n  });\n});\n\ndescribe(\"useStateValue\", () => {\n  it(\"returns value at specified path\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ user: { name: \"John\", age: 30 } }}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateValue(\"/user/name\"), {\n      wrapper,\n    });\n\n    expect(result.current).toBe(\"John\");\n  });\n\n  it(\"returns undefined for missing path\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{}}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateValue(\"/missing\"), { wrapper });\n\n    expect(result.current).toBeUndefined();\n  });\n\n  it(\"updates when state changes\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ count: 0 }}>{children}</StateProvider>\n    );\n\n    const { result, rerender } = renderHook(\n      () => ({\n        store: useStateStore(),\n        value: useStateValue<number>(\"/count\"),\n      }),\n      { wrapper },\n    );\n\n    expect(result.current.value).toBe(0);\n\n    act(() => {\n      result.current.store.set(\"/count\", 5);\n    });\n\n    rerender();\n    expect(result.current.value).toBe(5);\n  });\n});\n\ndescribe(\"useStateBinding\", () => {\n  it(\"returns tuple with value and setter for path\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ name: \"John\" }}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateBinding(\"/name\"), { wrapper });\n\n    const [value, setValue] = result.current;\n    expect(value).toBe(\"John\");\n    expect(typeof setValue).toBe(\"function\");\n  });\n\n  it(\"setter updates the value\", () => {\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={{ name: \"John\" }}>{children}</StateProvider>\n    );\n\n    const { result, rerender } = renderHook(() => useStateBinding(\"/name\"), {\n      wrapper,\n    });\n\n    act(() => {\n      const [, setValue] = result.current;\n      setValue(\"Alice\");\n    });\n\n    rerender();\n    const [value] = result.current;\n    expect(value).toBe(\"Alice\");\n  });\n});\n\n// ============================================================================\n// Controlled mode (external store)\n// ============================================================================\n\ndescribe(\"StateProvider (controlled mode)\", () => {\n  it(\"reads initial state from the external store\", () => {\n    const store = createStateStore({ count: 7 });\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.state).toEqual({ count: 7 });\n    expect(result.current.get(\"/count\")).toBe(7);\n  });\n\n  it(\"re-renders when the external store updates\", () => {\n    const store = createStateStore({ count: 0 });\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateValue<number>(\"/count\"), {\n      wrapper,\n    });\n\n    expect(result.current).toBe(0);\n\n    act(() => {\n      store.set(\"/count\", 42);\n    });\n\n    expect(result.current).toBe(42);\n  });\n\n  it(\"set() writes through to the external store\", () => {\n    const store = createStateStore({});\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.set(\"/name\", \"Alice\");\n    });\n\n    expect(store.getSnapshot().name).toBe(\"Alice\");\n    expect(result.current.state.name).toBe(\"Alice\");\n  });\n\n  it(\"update() writes multiple values through to the external store\", () => {\n    const store = createStateStore({});\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store}>{children}</StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.update({ \"/a\": 1, \"/b\": 2 });\n    });\n\n    expect(store.getSnapshot().a).toBe(1);\n    expect(store.getSnapshot().b).toBe(2);\n  });\n\n  it(\"does NOT call onStateChange when using an external store\", () => {\n    const store = createStateStore({});\n    const onStateChange = vi.fn();\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store} onStateChange={onStateChange}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    act(() => {\n      result.current.set(\"/x\", 99);\n    });\n\n    expect(onStateChange).not.toHaveBeenCalled();\n  });\n\n  it(\"ignores initialState when an external store is provided\", () => {\n    const store = createStateStore({ fromStore: true });\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider store={store} initialState={{ fromProp: true }}>\n        {children}\n      </StateProvider>\n    );\n\n    const { result } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.state).toEqual({ fromStore: true });\n    expect(result.current.get(\"/fromProp\")).toBeUndefined();\n  });\n});\n\n// ============================================================================\n// initialState sync fast-path\n// ============================================================================\n\ndescribe(\"StateProvider initialState sync\", () => {\n  it(\"does not re-flatten when initialState reference is unchanged\", () => {\n    const initialState = { count: 0 };\n    const wrapper = ({ children }: { children: React.ReactNode }) => (\n      <StateProvider initialState={initialState}>{children}</StateProvider>\n    );\n\n    const { result, rerender } = renderHook(() => useStateStore(), { wrapper });\n\n    expect(result.current.state).toEqual({ count: 0 });\n\n    act(() => {\n      result.current.set(\"/count\", 5);\n    });\n\n    rerender();\n    expect(result.current.state.count).toBe(5);\n  });\n});\n"
  },
  {
    "path": "packages/react-state/src/index.tsx",
    "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useContext,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useSyncExternalStore,\n  type ReactNode,\n} from \"react\";\nimport {\n  getByPath,\n  createStateStore,\n  type StateModel,\n  type StateStore,\n} from \"@json-render/core\";\nimport { flattenToPointers } from \"@json-render/core/store-utils\";\n\n/**\n * State context value\n */\nexport interface StateContextValue {\n  /** The current state model */\n  state: StateModel;\n  /** Get a value by path */\n  get: (path: string) => unknown;\n  /** Set a value by path */\n  set: (path: string, value: unknown) => void;\n  /** Update multiple values at once */\n  update: (updates: Record<string, unknown>) => void;\n  /** Return the live state snapshot from the underlying store (not the React render snapshot). */\n  getSnapshot: () => StateModel;\n}\n\nconst StateContext = createContext<StateContextValue | null>(null);\n\n/**\n * Props for StateProvider\n */\nexport interface StateProviderProps {\n  /**\n   * External store that owns the state. When provided, the provider operates\n   * in **controlled mode** — `initialState` and `onStateChange` are ignored\n   * and the store is the single source of truth.\n   */\n  store?: StateStore;\n  /** Initial state model (used only in uncontrolled mode) */\n  initialState?: StateModel;\n  /**\n   * Callback when state changes (used only in uncontrolled mode).\n   * Called once per `set` or `update` with all changed entries.\n   */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  children: ReactNode;\n}\n\nfunction computeInitialFlat(\n  isControlled: boolean,\n  initialState: StateModel,\n): Record<string, unknown> | null {\n  if (isControlled) return null;\n  if (Object.keys(initialState).length === 0) return {};\n  return flattenToPointers(initialState);\n}\n\n/**\n * Provider for state model context.\n *\n * Supports two modes:\n * - **Controlled**: pass a `store` prop (e.g. backed by Redux / Zustand).\n * - **Uncontrolled** (default): omit `store` and optionally pass\n *   `initialState` / `onStateChange`.\n */\nexport function StateProvider({\n  store: externalStore,\n  initialState = {},\n  onStateChange,\n  children,\n}: StateProviderProps) {\n  const internalStoreRef = useRef<StateStore | undefined>(undefined);\n  if (!externalStore && !internalStoreRef.current) {\n    internalStoreRef.current = createStateStore(initialState);\n  }\n\n  const store = externalStore ?? internalStoreRef.current!;\n\n  // Refs for stable callback identity — callbacks never change regardless of\n  // whether the consumer passes a new store / onStateChange reference.\n  const storeRef = useRef(store);\n  storeRef.current = store;\n\n  const isControlledRef = useRef(!!externalStore);\n  isControlledRef.current = !!externalStore;\n\n  const initialModeRef = useRef(externalStore ? \"controlled\" : \"uncontrolled\");\n  const modeWarnedRef = useRef(false);\n  if (process.env.NODE_ENV !== \"production\") {\n    const currentMode = externalStore ? \"controlled\" : \"uncontrolled\";\n    if (currentMode !== initialModeRef.current && !modeWarnedRef.current) {\n      modeWarnedRef.current = true;\n      console.warn(\n        `StateProvider: switching from ${initialModeRef.current} to ${currentMode} mode is not supported.`,\n      );\n    }\n  }\n\n  const prevInitialStateRef = useRef(initialState);\n  const prevFlatRef = useRef<Record<string, unknown> | null>(\n    computeInitialFlat(!!externalStore, initialState),\n  );\n  useEffect(() => {\n    if (externalStore) return;\n    if (initialState === prevInitialStateRef.current) return;\n    prevInitialStateRef.current = initialState;\n    const nextFlat =\n      initialState && Object.keys(initialState).length > 0\n        ? flattenToPointers(initialState)\n        : {};\n    const prevFlat = prevFlatRef.current ?? {};\n    const allKeys = new Set([\n      ...Object.keys(prevFlat),\n      ...Object.keys(nextFlat),\n    ]);\n    const updates: Record<string, unknown> = {};\n    for (const key of allKeys) {\n      if (prevFlat[key] !== nextFlat[key]) {\n        updates[key] = key in nextFlat ? nextFlat[key] : undefined;\n      }\n    }\n    prevFlatRef.current = nextFlat;\n    if (Object.keys(updates).length > 0) {\n      store.update(updates);\n    }\n  }, [externalStore, initialState, store]);\n\n  const state = useSyncExternalStore(\n    store.subscribe,\n    store.getSnapshot,\n    store.getServerSnapshot ?? store.getSnapshot,\n  );\n\n  const onStateChangeRef = useRef(onStateChange);\n  onStateChangeRef.current = onStateChange;\n\n  const set = useCallback((path: string, value: unknown) => {\n    const s = storeRef.current;\n    const prev = s.getSnapshot();\n    s.set(path, value);\n    if (!isControlledRef.current && s.getSnapshot() !== prev) {\n      onStateChangeRef.current?.([{ path, value }]);\n    }\n  }, []);\n\n  const update = useCallback((updates: Record<string, unknown>) => {\n    const s = storeRef.current;\n    const prev = s.getSnapshot();\n    s.update(updates);\n    if (!isControlledRef.current && s.getSnapshot() !== prev) {\n      const changes: Array<{ path: string; value: unknown }> = [];\n      for (const [path, value] of Object.entries(updates)) {\n        if (getByPath(prev, path) !== value) {\n          changes.push({ path, value });\n        }\n      }\n      if (changes.length > 0) {\n        onStateChangeRef.current?.(changes);\n      }\n    }\n  }, []);\n\n  const get = useCallback((path: string) => storeRef.current.get(path), []);\n\n  const getSnapshot = useCallback(() => storeRef.current.getSnapshot(), []);\n\n  const value = useMemo<StateContextValue>(\n    () => ({ state, get, set, update, getSnapshot }),\n    [state, get, set, update, getSnapshot],\n  );\n\n  return (\n    <StateContext.Provider value={value}>{children}</StateContext.Provider>\n  );\n}\n\n/**\n * Hook to access the state context\n */\nexport function useStateStore(): StateContextValue {\n  const ctx = useContext(StateContext);\n  if (!ctx) {\n    throw new Error(\"useStateStore must be used within a StateProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Hook to get a value from the state model\n */\nexport function useStateValue<T>(path: string): T | undefined {\n  const { state } = useStateStore();\n  return getByPath(state, path) as T | undefined;\n}\n\n/**\n * Hook to get and set a value from the state model (like useState).\n *\n * @deprecated Use {@link useBoundProp} with `$bindState` expressions instead.\n * `useStateBinding` takes a raw state path string, while `useBoundProp` works\n * with the renderer's `bindings` map and supports both `$bindState` and\n * `$bindItem` expressions.\n */\nexport function useStateBinding<T>(\n  path: string,\n): [T | undefined, (value: T) => void] {\n  const { state, set } = useStateStore();\n  const value = getByPath(state, path) as T | undefined;\n  const setValue = useCallback(\n    (newValue: T) => set(path, newValue),\n    [path, set],\n  );\n  return [value, setValue];\n}\n"
  },
  {
    "path": "packages/react-state/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-state/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.tsx\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"@json-render/core\", \"@json-render/core/store-utils\", \"react\"],\n});\n"
  },
  {
    "path": "packages/react-state/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"path\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      react: path.resolve(__dirname, \"../../node_modules/react\"),\n      \"react-dom\": path.resolve(__dirname, \"../../node_modules/react-dom\"),\n    },\n  },\n  test: {\n    globals: true,\n    environment: \"jsdom\",\n    include: [\"src/**/*.test.tsx\"],\n  },\n});\n"
  },
  {
    "path": "packages/react-three-fiber/CHANGELOG.md",
    "content": "# @json-render/react-three-fiber\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.13.0\n\n### Minor Changes\n\n- 5b32de8: Add SolidJS and React Three Fiber renderers, plus strict JSON Schema mode for LLM structured outputs.\n\n  ### New:\n  - **`@json-render/solid`** -- SolidJS renderer. JSON becomes Solid components with reactive rendering, schema export, and full catalog support.\n  - **`@json-render/react-three-fiber`** -- React Three Fiber renderer. JSON becomes 3D scenes with 19 built-in components for meshes, lights, models, environments, text, cameras, and controls.\n\n  ### Improved:\n  - **`@json-render/core`** -- `jsonSchema({ strict: true })` produces a JSON Schema subset compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic). Ensures `additionalProperties: false` on every object and all properties listed in `required`.\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n"
  },
  {
    "path": "packages/react-three-fiber/README.md",
    "content": "# @json-render/react-three-fiber\n\nReact Three Fiber renderer for [`@json-render/core`](https://json-render.dev). JSON becomes 3D scenes.\n\n19 built-in components for meshes, lights, models, environments, text, cameras, and controls.\n\n## Installation\n\n```bash\nnpm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod\n```\n\n## Usage\n\n```tsx\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema, defineRegistry } from \"@json-render/react\";\nimport {\n  threeComponentDefinitions,\n  threeComponents,\n  ThreeCanvas,\n} from \"@json-render/react-three-fiber\";\n\n// Build catalog with 3D components\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// Register 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// Render\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## Components\n\n**Primitives:** Box, Sphere, Cylinder, Cone, Torus, Plane, Capsule\n\n**Lights:** AmbientLight, DirectionalLight, PointLight, SpotLight\n\n**Container:** Group\n\n**Model:** Model (GLTF/GLB)\n\n**Environment:** Environment, Fog, GridHelper\n\n**Text:** Text3D\n\n**Camera/Controls:** PerspectiveCamera, OrbitControls\n\n## Docs\n\nFull API reference: [json-render.dev/docs/api/react-three-fiber](https://json-render.dev/docs/api/react-three-fiber)\n"
  },
  {
    "path": "packages/react-three-fiber/package.json",
    "content": "{\n  \"name\": \"@json-render/react-three-fiber\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.\",\n  \"keywords\": [\n    \"json\",\n    \"3d\",\n    \"three\",\n    \"threejs\",\n    \"react-three-fiber\",\n    \"r3f\",\n    \"webgl\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/react-three-fiber\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@react-three/postprocessing\": \"^3.0.4\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@react-three/drei\": \"^10.7.7\",\n    \"@react-three/fiber\": \"^9.5.0\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/three\": \"^0.183.1\",\n    \"three\": \"^0.183.2\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"@react-three/drei\": \">=9.0.0\",\n    \"@react-three/fiber\": \">=8.0.0\",\n    \"react\": \"^19.0.0\",\n    \"three\": \">=0.160.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/react-three-fiber/src/catalog.ts",
    "content": "import { z } from \"zod\";\nimport {\n  vector3Schema,\n  materialSchema,\n  transformProps,\n  shadowProps,\n} from \"./schemas\";\n\n/**\n * React Three Fiber component definitions for json-render catalogs.\n *\n * These can be used directly or extended with custom 3D components.\n * All components render as React Three Fiber elements.\n */\nexport const threeComponentDefinitions = {\n  // ===========================================================================\n  // Primitives\n  // ===========================================================================\n\n  Box: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      depth: z.number().nullable(),\n    }),\n    description:\n      \"Box mesh. Defaults to 1x1x1. Use material for appearance, transform props for placement.\",\n    example: {\n      position: [0, 0.5, 0],\n      material: { color: \"#4488ff\" },\n    },\n  },\n\n  Sphere: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radius: z.number().nullable(),\n      widthSegments: z.number().nullable(),\n      heightSegments: z.number().nullable(),\n    }),\n    description: \"Sphere mesh. Defaults to radius 1.\",\n    example: {\n      position: [0, 1, 0],\n      radius: 0.5,\n      material: { color: \"#ff4444\" },\n    },\n  },\n\n  Cylinder: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radiusTop: z.number().nullable(),\n      radiusBottom: z.number().nullable(),\n      height: z.number().nullable(),\n      radialSegments: z.number().nullable(),\n    }),\n    description: \"Cylinder mesh. Use radiusTop/radiusBottom for tapering.\",\n    example: {\n      position: [0, 0.5, 0],\n      height: 1,\n      material: { color: \"#44ff44\" },\n    },\n  },\n\n  Cone: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radius: z.number().nullable(),\n      height: z.number().nullable(),\n      radialSegments: z.number().nullable(),\n    }),\n    description: \"Cone mesh.\",\n    example: {\n      position: [0, 0.5, 0],\n      material: { color: \"#ffaa00\" },\n    },\n  },\n\n  Torus: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radius: z.number().nullable(),\n      tube: z.number().nullable(),\n      radialSegments: z.number().nullable(),\n      tubularSegments: z.number().nullable(),\n    }),\n    description: \"Torus (donut) mesh.\",\n    example: {\n      position: [0, 1, 0],\n      material: { color: \"#ff44ff\" },\n    },\n  },\n\n  Plane: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Flat plane mesh. Useful for floors/walls. Defaults to XY plane; rotate [-Math.PI/2, 0, 0] for ground. Pass MeshPortalMaterial as child for portal surfaces.\",\n    example: {\n      position: [0, 0, 0],\n      rotation: [-1.5708, 0, 0],\n      scale: [10, 10, 1],\n      material: { color: \"#888888\" },\n      receiveShadow: true,\n    },\n  },\n\n  Capsule: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radius: z.number().nullable(),\n      length: z.number().nullable(),\n      capSegments: z.number().nullable(),\n      radialSegments: z.number().nullable(),\n    }),\n    description: \"Capsule mesh (cylinder with hemispherical caps).\",\n    example: {\n      position: [0, 1, 0],\n      material: { color: \"#00cccc\" },\n    },\n  },\n\n  // ===========================================================================\n  // Lights\n  // ===========================================================================\n\n  AmbientLight: {\n    props: z.object({\n      color: z.string().nullable(),\n      intensity: z.number().nullable(),\n    }),\n    description:\n      \"Ambient light that illuminates all objects equally. No position needed.\",\n    example: { color: \"#ffffff\", intensity: 0.5 },\n  },\n\n  DirectionalLight: {\n    props: z.object({\n      ...transformProps,\n      color: z.string().nullable(),\n      intensity: z.number().nullable(),\n      castShadow: z.boolean().nullable(),\n    }),\n    description:\n      \"Directional light (like sunlight). Position determines the direction it shines from.\",\n    example: {\n      position: [5, 10, 5],\n      intensity: 1,\n      castShadow: true,\n    },\n  },\n\n  PointLight: {\n    props: z.object({\n      ...transformProps,\n      color: z.string().nullable(),\n      intensity: z.number().nullable(),\n      distance: z.number().nullable(),\n      decay: z.number().nullable(),\n      castShadow: z.boolean().nullable(),\n    }),\n    description: \"Point light that radiates in all directions from a position.\",\n    example: {\n      position: [0, 3, 0],\n      intensity: 1,\n      distance: 10,\n    },\n  },\n\n  SpotLight: {\n    props: z.object({\n      ...transformProps,\n      color: z.string().nullable(),\n      intensity: z.number().nullable(),\n      distance: z.number().nullable(),\n      decay: z.number().nullable(),\n      angle: z.number().nullable(),\n      penumbra: z.number().nullable(),\n      castShadow: z.boolean().nullable(),\n    }),\n    description:\n      \"Spot light emitting a cone of light. Angle in radians (default ~Math.PI/3).\",\n    example: {\n      position: [0, 5, 0],\n      intensity: 1,\n      angle: 0.5,\n      penumbra: 0.5,\n      castShadow: true,\n    },\n  },\n\n  // ===========================================================================\n  // Container\n  // ===========================================================================\n\n  Group: {\n    props: z.object({\n      ...transformProps,\n    }),\n    slots: [\"default\"],\n    description:\n      \"Container for grouping child objects. Transforms apply to all children.\",\n    example: { position: [0, 0, 0] },\n  },\n\n  // ===========================================================================\n  // Model\n  // ===========================================================================\n\n  Model: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      url: z.string(),\n    }),\n    description:\n      \"GLTF/GLB 3D model loader. Provide a URL to a .glb or .gltf file.\",\n    example: {\n      url: \"/models/robot.glb\",\n      position: [0, 0, 0],\n      scale: [1, 1, 1],\n    },\n  },\n\n  // ===========================================================================\n  // Environment\n  // ===========================================================================\n\n  Environment: {\n    props: z.object({\n      preset: z\n        .enum([\n          \"apartment\",\n          \"city\",\n          \"dawn\",\n          \"forest\",\n          \"lobby\",\n          \"night\",\n          \"park\",\n          \"studio\",\n          \"sunset\",\n          \"warehouse\",\n        ])\n        .nullable(),\n      background: z.boolean().nullable(),\n      blur: z.number().nullable(),\n      intensity: z.number().nullable(),\n    }),\n    description:\n      \"HDRI environment map for lighting and background. Choose a preset or provide a custom HDRI.\",\n    example: { preset: \"sunset\", background: true },\n  },\n\n  Fog: {\n    props: z.object({\n      color: z.string().nullable(),\n      near: z.number().nullable(),\n      far: z.number().nullable(),\n    }),\n    description:\n      \"Linear fog effect. Objects fade between near and far distances.\",\n    example: { color: \"#cccccc\", near: 10, far: 50 },\n  },\n\n  GridHelper: {\n    props: z.object({\n      ...transformProps,\n      size: z.number().nullable(),\n      divisions: z.number().nullable(),\n      color: z.string().nullable(),\n      secondaryColor: z.string().nullable(),\n    }),\n    description: \"Visual grid for reference. Renders on the XZ plane.\",\n    example: { size: 10, divisions: 10 },\n  },\n\n  // ===========================================================================\n  // Text\n  // ===========================================================================\n\n  Text3D: {\n    props: z.object({\n      ...transformProps,\n      text: z.string(),\n      fontSize: z.number().nullable(),\n      color: z.string().nullable(),\n      anchorX: z.enum([\"left\", \"center\", \"right\"]).nullable(),\n      anchorY: z\n        .enum([\"top\", \"top-baseline\", \"middle\", \"bottom-baseline\", \"bottom\"])\n        .nullable(),\n      maxWidth: z.number().nullable(),\n    }),\n    description:\n      \"3D text rendered in the scene. Uses SDF text for crisp rendering at any size.\",\n    example: {\n      text: \"Hello World\",\n      fontSize: 1,\n      color: \"#ffffff\",\n      position: [0, 2, 0],\n    },\n  },\n\n  // ===========================================================================\n  // Effects / Atmosphere\n  // ===========================================================================\n\n  Sparkles: {\n    props: z.object({\n      ...transformProps,\n      count: z.number().nullable(),\n      speed: z.number().nullable(),\n      opacity: z.number().nullable(),\n      color: z.string().nullable(),\n      size: z.number().nullable(),\n      noise: z.number().nullable(),\n    }),\n    description:\n      \"Floating particle sparkles. Great for magic, snow, or ambient effects.\",\n    example: {\n      count: 100,\n      speed: 0.5,\n      size: 2,\n      color: \"#ffffff\",\n      scale: [5, 5, 5],\n    },\n  },\n\n  Stars: {\n    props: z.object({\n      radius: z.number().nullable(),\n      depth: z.number().nullable(),\n      count: z.number().nullable(),\n      factor: z.number().nullable(),\n      saturation: z.number().nullable(),\n      fade: z.boolean().nullable(),\n      speed: z.number().nullable(),\n    }),\n    description:\n      \"Starfield background. Renders thousands of stars in a sphere around the scene.\",\n    example: { radius: 100, depth: 50, count: 5000, factor: 4, fade: true },\n  },\n\n  Sky: {\n    props: z.object({\n      distance: z.number().nullable(),\n      sunPosition: vector3Schema.nullable(),\n      inclination: z.number().nullable(),\n      azimuth: z.number().nullable(),\n      mieCoefficient: z.number().nullable(),\n      mieDirectionalG: z.number().nullable(),\n      rayleigh: z.number().nullable(),\n      turbidity: z.number().nullable(),\n    }),\n    description:\n      \"Procedural sky with sun. Control sun position, haze, and scattering.\",\n    example: { sunPosition: [100, 20, 100], turbidity: 8, rayleigh: 2 },\n  },\n\n  Cloud: {\n    props: z.object({\n      ...transformProps,\n      seed: z.number().nullable(),\n      segments: z.number().nullable(),\n      bounds: vector3Schema.nullable(),\n      volume: z.number().nullable(),\n      speed: z.number().nullable(),\n      fade: z.number().nullable(),\n      opacity: z.number().nullable(),\n      color: z.string().nullable(),\n      growth: z.number().nullable(),\n    }),\n    description:\n      \"Volumetric cloud. Use multiple for a cloudscape. Wrap in a Clouds parent for batching.\",\n    example: {\n      position: [0, 5, 0],\n      speed: 0.2,\n      opacity: 0.6,\n      color: \"#ffffff\",\n    },\n  },\n\n  // ===========================================================================\n  // Special Materials (geometry + material combos)\n  // ===========================================================================\n\n  GlassSphere: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      radius: z.number().nullable(),\n      widthSegments: z.number().nullable(),\n      heightSegments: z.number().nullable(),\n      color: z.string().nullable(),\n      transmission: z.number().nullable(),\n      thickness: z.number().nullable(),\n      roughness: z.number().nullable(),\n      chromaticAberration: z.number().nullable(),\n      ior: z.number().nullable(),\n      distortion: z.number().nullable(),\n      distortionScale: z.number().nullable(),\n      temporalDistortion: z.number().nullable(),\n      samples: z.number().nullable(),\n      resolution: z.number().nullable(),\n    }),\n    description:\n      \"Glass sphere with transmission/refraction. Creates photorealistic glass effect.\",\n    example: {\n      position: [0, 1, 0],\n      radius: 1,\n      transmission: 1,\n      thickness: 0.5,\n      roughness: 0,\n      chromaticAberration: 0.06,\n    },\n  },\n\n  GlassBox: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      depth: z.number().nullable(),\n      color: z.string().nullable(),\n      transmission: z.number().nullable(),\n      thickness: z.number().nullable(),\n      roughness: z.number().nullable(),\n      chromaticAberration: z.number().nullable(),\n      ior: z.number().nullable(),\n      distortion: z.number().nullable(),\n      distortionScale: z.number().nullable(),\n      temporalDistortion: z.number().nullable(),\n      samples: z.number().nullable(),\n      resolution: z.number().nullable(),\n    }),\n    description:\n      \"Glass box with transmission/refraction. Photorealistic glass cuboid.\",\n    example: {\n      position: [0, 0.5, 0],\n      width: 1,\n      height: 1,\n      depth: 1,\n      transmission: 1,\n      thickness: 0.5,\n    },\n  },\n\n  DistortSphere: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      radius: z.number().nullable(),\n      widthSegments: z.number().nullable(),\n      heightSegments: z.number().nullable(),\n      color: z.string().nullable(),\n      speed: z.number().nullable(),\n      distort: z.number().nullable(),\n      metalness: z.number().nullable(),\n      roughness: z.number().nullable(),\n    }),\n    description:\n      \"Animated distorting sphere. Organic, blobby look -- like liquid metal.\",\n    example: {\n      position: [0, 1, 0],\n      radius: 1,\n      color: \"#ff6600\",\n      speed: 2,\n      distort: 0.5,\n    },\n  },\n\n  // ===========================================================================\n  // Extended Geometry\n  // ===========================================================================\n\n  TorusKnot: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      radius: z.number().nullable(),\n      tube: z.number().nullable(),\n      tubularSegments: z.number().nullable(),\n      radialSegments: z.number().nullable(),\n      p: z.number().nullable(),\n      q: z.number().nullable(),\n    }),\n    description:\n      \"Torus knot mesh. A continuous 3D curve that creates intricate knot shapes via p and q parameters.\",\n    example: {\n      position: [0, 1, 0],\n      radius: 1,\n      tube: 0.3,\n      p: 2,\n      q: 3,\n      material: { color: \"#ff44ff\", metalness: 0.8, roughness: 0.2 },\n    },\n  },\n\n  RoundedBox: {\n    props: z.object({\n      ...transformProps,\n      ...shadowProps,\n      material: materialSchema.nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      depth: z.number().nullable(),\n      radius: z.number().nullable(),\n      smoothness: z.number().nullable(),\n    }),\n    description:\n      \"Box with rounded edges. More polished look than a plain box. Great for product-style scenes.\",\n    example: {\n      position: [0, 0.5, 0],\n      width: 1,\n      height: 1,\n      depth: 1,\n      radius: 0.1,\n      material: { color: \"#4488ff\" },\n    },\n  },\n\n  // ===========================================================================\n  // Shadows / Staging\n  // ===========================================================================\n\n  ContactShadows: {\n    props: z.object({\n      ...transformProps,\n      opacity: z.number().nullable(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      blur: z.number().nullable(),\n      near: z.number().nullable(),\n      far: z.number().nullable(),\n      smooth: z.boolean().nullable(),\n      resolution: z.number().nullable(),\n      frames: z.number().nullable(),\n      color: z.string().nullable(),\n    }),\n    description:\n      \"Soft contact shadows projected onto the ground plane. Place at y=0 under objects.\",\n    example: {\n      position: [0, 0, 0],\n      opacity: 0.5,\n      blur: 2,\n      width: 10,\n      height: 10,\n    },\n  },\n\n  Float: {\n    props: z.object({\n      ...transformProps,\n      speed: z.number().nullable(),\n      rotationIntensity: z.number().nullable(),\n      floatIntensity: z.number().nullable(),\n      enabled: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Wrapper that makes children gently float and bob. Adds organic motion to any object.\",\n    example: { speed: 1.5, rotationIntensity: 1, floatIntensity: 1 },\n  },\n\n  ReflectorPlane: {\n    props: z.object({\n      ...transformProps,\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n      color: z.string().nullable(),\n      resolution: z.number().nullable(),\n      blur: z.number().nullable(),\n      mirror: z.number().nullable(),\n      mixBlur: z.number().nullable(),\n      mixStrength: z.number().nullable(),\n      depthScale: z.number().nullable(),\n      metalness: z.number().nullable(),\n      roughness: z.number().nullable(),\n    }),\n    description:\n      \"Reflective floor/surface. Real-time mirror reflections on a plane. Rotate [-PI/2,0,0] for ground.\",\n    example: {\n      position: [0, 0, 0],\n      rotation: [-1.5708, 0, 0],\n      width: 20,\n      height: 20,\n      mirror: 0.5,\n      blur: [300, 100],\n      resolution: 1024,\n    },\n  },\n\n  Backdrop: {\n    props: z.object({\n      ...transformProps,\n      floor: z.number().nullable(),\n      segments: z.number().nullable(),\n      receiveShadow: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Curved studio backdrop for product-style scenes. Set floor for how much bends.\",\n    example: { floor: 0.25, segments: 20, receiveShadow: true },\n  },\n\n  // ===========================================================================\n  // Crazy / Magic\n  // ===========================================================================\n\n  MeshPortalMaterial: {\n    props: z.object({\n      blend: z.number().nullable(),\n      blur: z.number().nullable(),\n      resolution: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Renders children into a portal surface (like a window to another world). Use as a child of a Mesh.\",\n    example: { blend: 0 },\n  },\n\n  HtmlLabel: {\n    props: z.object({\n      ...transformProps,\n      text: z.string(),\n      transform: z.boolean().nullable(),\n      distanceFactor: z.number().nullable(),\n      color: z.string().nullable(),\n      fontSize: z.number().nullable(),\n      center: z.boolean().nullable(),\n    }),\n    description: \"Renders actual HTML/DOM text floating in the 3D scene.\",\n    example: { text: \"Hello World\", transform: true, distanceFactor: 10 },\n  },\n\n  // ===========================================================================\n  // Post-Processing\n  // ===========================================================================\n\n  EffectComposer: {\n    props: z.object({\n      enabled: z.boolean().nullable(),\n      multisampling: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Wrapper for post-processing effects. Add effects as children.\",\n    example: { multisampling: 8 },\n  },\n\n  Bloom: {\n    props: z.object({\n      intensity: z.number().nullable(),\n      luminanceThreshold: z.number().nullable(),\n      luminanceSmoothing: z.number().nullable(),\n      mipmapBlur: z.boolean().nullable(),\n    }),\n    description: \"Bloom post-processing effect. Makes emissive materials glow.\",\n    example: { intensity: 1.5, luminanceThreshold: 0.1, mipmapBlur: true },\n  },\n\n  Glitch: {\n    props: z.object({\n      delay: z.tuple([z.number(), z.number()]).nullable(),\n      duration: z.tuple([z.number(), z.number()]).nullable(),\n      strength: z.tuple([z.number(), z.number()]).nullable(),\n      active: z.boolean().nullable(),\n      ratio: z.number().nullable(),\n    }),\n    description: \"Cyberpunk glitch post-processing effect.\",\n    example: { active: true },\n  },\n\n  Vignette: {\n    props: z.object({\n      offset: z.number().nullable(),\n      darkness: z.number().nullable(),\n    }),\n    description: \"Vignette post-processing effect (darkened corners).\",\n    example: { offset: 0.5, darkness: 0.5 },\n  },\n\n  // ===========================================================================\n  // Animation Wrappers\n  // ===========================================================================\n\n  WarpTunnel: {\n    props: z.object({\n      ...transformProps,\n      ringCount: z.number().nullable(),\n      radius: z.number().nullable(),\n      length: z.number().nullable(),\n      speed: z.number().nullable(),\n      tubeRadius: z.number().nullable(),\n      color1: z.string().nullable(),\n      color2: z.string().nullable(),\n    }),\n    description:\n      \"Animated neon tunnel flythrough. Rings rush toward the camera creating a hyperspace warp effect.\",\n    example: {\n      speed: 8,\n      ringCount: 80,\n      radius: 3,\n      color1: \"#00ffff\",\n      color2: \"#ff00ff\",\n    },\n  },\n\n  Spin: {\n    props: z.object({\n      ...transformProps,\n      speed: z.number().nullable(),\n      axis: z.enum([\"x\", \"y\", \"z\"]).nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Wrapper that continuously spins children around an axis. Speed in radians/second.\",\n    example: { speed: 1, axis: \"y\" },\n  },\n\n  Orbit: {\n    props: z.object({\n      ...transformProps,\n      speed: z.number().nullable(),\n      radius: z.number().nullable(),\n      tilt: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Wrapper that orbits children around the Y axis at a given radius and speed.\",\n    example: { speed: 1, radius: 3, tilt: 0.5 },\n  },\n\n  Pulse: {\n    props: z.object({\n      ...transformProps,\n      speed: z.number().nullable(),\n      min: z.number().nullable(),\n      max: z.number().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Wrapper that pulses children's scale between min and max values.\",\n    example: { speed: 1, min: 0.8, max: 1.2 },\n  },\n\n  CameraShake: {\n    props: z.object({\n      intensity: z.number().nullable(),\n      maxYaw: z.number().nullable(),\n      maxPitch: z.number().nullable(),\n      maxRoll: z.number().nullable(),\n    }),\n    description:\n      \"Adds camera shake/vibration. Great for speed, impact, or unease effects.\",\n    example: { intensity: 0.5, maxYaw: 0.1, maxPitch: 0.1, maxRoll: 0.1 },\n  },\n\n  // ===========================================================================\n  // Camera / Controls\n  // ===========================================================================\n\n  PerspectiveCamera: {\n    props: z.object({\n      ...transformProps,\n      fov: z.number().nullable(),\n      near: z.number().nullable(),\n      far: z.number().nullable(),\n      makeDefault: z.boolean().nullable(),\n    }),\n    description:\n      \"Perspective camera. Set makeDefault to use as the scene's main camera.\",\n    example: {\n      position: [5, 5, 5],\n      fov: 50,\n      makeDefault: true,\n    },\n  },\n\n  OrbitControls: {\n    props: z.object({\n      enableDamping: z.boolean().nullable(),\n      dampingFactor: z.number().nullable(),\n      enableZoom: z.boolean().nullable(),\n      enablePan: z.boolean().nullable(),\n      enableRotate: z.boolean().nullable(),\n      minDistance: z.number().nullable(),\n      maxDistance: z.number().nullable(),\n      minPolarAngle: z.number().nullable(),\n      maxPolarAngle: z.number().nullable(),\n      autoRotate: z.boolean().nullable(),\n      autoRotateSpeed: z.number().nullable(),\n      target: z.tuple([z.number(), z.number(), z.number()]).nullable(),\n    }),\n    description:\n      \"Orbit camera controls. Allows the user to rotate, zoom, and pan around the scene.\",\n    example: { enableDamping: true, autoRotate: false },\n  },\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type ComponentDefinition = {\n  props: z.ZodType;\n  slots?: string[];\n  description: string;\n  example?: Record<string, unknown>;\n};\n\n/**\n * Infer the props type for a Three component by name.\n *\n * @example\n * ```ts\n * type BoxProps = ThreeProps<\"Box\">;\n * ```\n */\nexport type ThreeProps<K extends keyof typeof threeComponentDefinitions> =\n  z.output<(typeof threeComponentDefinitions)[K][\"props\"]>;\n"
  },
  {
    "path": "packages/react-three-fiber/src/components.tsx",
    "content": "// @ts-nocheck\nimport React, { type ReactNode, Suspense, useRef, useMemo } from \"react\";\nimport { useFrame } from \"@react-three/fiber\";\nimport {\n  Environment as DreiEnvironment,\n  OrbitControls as DreiOrbitControls,\n  PerspectiveCamera as DreiPerspectiveCamera,\n  Text as DreiText,\n  Gltf,\n  Sparkles as DreiSparkles,\n  Stars as DreiStars,\n  Sky as DreiSky,\n  Cloud as DreiCloud,\n  Clouds as DreiClouds,\n  Float as DreiFloat,\n  ContactShadows as DreiContactShadows,\n  MeshTransmissionMaterial,\n  MeshDistortMaterial,\n  MeshReflectorMaterial,\n  MeshPortalMaterial as DreiMeshPortalMaterial,\n  Html as DreiHtml,\n  RoundedBox as DreiRoundedBox,\n  Backdrop as DreiBackdrop,\n  CameraShake as DreiCameraShake,\n} from \"@react-three/drei\";\n// @ts-ignore\nimport * as postprocessing from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport type { BaseComponentProps } from \"@json-render/react\";\nimport type { ThreeProps } from \"./catalog\";\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\ntype Vec3 = [number, number, number];\n\nconst DEFAULT_POS: Vec3 = [0, 0, 0];\nconst DEFAULT_ROT: Vec3 = [0, 0, 0];\nconst DEFAULT_SCALE: Vec3 = [1, 1, 1];\n\nfunction pos(v: Vec3 | null | undefined): Vec3 {\n  return v ?? DEFAULT_POS;\n}\n\nfunction rot(v: Vec3 | null | undefined): Vec3 {\n  return v ?? DEFAULT_ROT;\n}\n\nfunction scl(v: Vec3 | null | undefined): Vec3 {\n  return v ?? DEFAULT_SCALE;\n}\n\nfunction MaterialComponent({\n  material,\n}: {\n  material: ThreeProps<\"Box\">[\"material\"] | null | undefined;\n}) {\n  if (!material) return <meshStandardMaterial />;\n  return (\n    <meshStandardMaterial\n      color={material.color ?? \"#ffffff\"}\n      metalness={material.metalness ?? 0}\n      roughness={material.roughness ?? 1}\n      emissive={material.emissive ?? \"#000000\"}\n      emissiveIntensity={material.emissiveIntensity ?? 1}\n      opacity={material.opacity ?? 1}\n      transparent={material.transparent ?? false}\n      wireframe={material.wireframe ?? false}\n    />\n  );\n}\n\n// =============================================================================\n// Component Implementations\n// =============================================================================\n\n/**\n * React Three Fiber component implementations for json-render.\n *\n * Pass to `defineRegistry()` from `@json-render/react` to create a\n * component registry for rendering JSON specs as 3D scenes.\n *\n * @example\n * ```ts\n * import { defineRegistry } from \"@json-render/react\";\n * import { threeComponents } from \"@json-render/react-three-fiber\";\n *\n * const { registry } = defineRegistry(catalog, {\n *   components: { ...threeComponents },\n * });\n * ```\n */\nexport const threeComponents = {\n  // ── Primitives ─────────────────────────────────────────────────────────\n\n  Box: ({ props }: BaseComponentProps<ThreeProps<\"Box\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <boxGeometry\n        args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  Sphere: ({ props }: BaseComponentProps<ThreeProps<\"Sphere\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <sphereGeometry\n        args={[\n          props.radius ?? 1,\n          props.widthSegments ?? 32,\n          props.heightSegments ?? 16,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  Cylinder: ({ props }: BaseComponentProps<ThreeProps<\"Cylinder\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <cylinderGeometry\n        args={[\n          props.radiusTop ?? 1,\n          props.radiusBottom ?? 1,\n          props.height ?? 1,\n          props.radialSegments ?? 32,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  Cone: ({ props }: BaseComponentProps<ThreeProps<\"Cone\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <coneGeometry\n        args={[\n          props.radius ?? 1,\n          props.height ?? 1,\n          props.radialSegments ?? 32,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  Torus: ({ props }: BaseComponentProps<ThreeProps<\"Torus\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <torusGeometry\n        args={[\n          props.radius ?? 1,\n          props.tube ?? 0.4,\n          props.radialSegments ?? 16,\n          props.tubularSegments ?? 48,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  Plane: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Plane\">> & { children?: ReactNode }) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <planeGeometry args={[props.width ?? 1, props.height ?? 1]} />\n      {children ?? <MaterialComponent material={props.material} />}\n    </mesh>\n  ),\n\n  Capsule: ({ props }: BaseComponentProps<ThreeProps<\"Capsule\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <capsuleGeometry\n        args={[\n          props.radius ?? 0.5,\n          props.length ?? 1,\n          props.capSegments ?? 4,\n          props.radialSegments ?? 16,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  // ── Lights ─────────────────────────────────────────────────────────────\n\n  AmbientLight: ({ props }: BaseComponentProps<ThreeProps<\"AmbientLight\">>) => (\n    <ambientLight\n      color={props.color ?? \"#ffffff\"}\n      intensity={props.intensity ?? 1}\n    />\n  ),\n\n  DirectionalLight: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"DirectionalLight\">>) => (\n    <directionalLight\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      color={props.color ?? \"#ffffff\"}\n      intensity={props.intensity ?? 1}\n      castShadow={props.castShadow ?? false}\n    />\n  ),\n\n  PointLight: ({ props }: BaseComponentProps<ThreeProps<\"PointLight\">>) => (\n    <pointLight\n      position={pos(props.position)}\n      color={props.color ?? \"#ffffff\"}\n      intensity={props.intensity ?? 1}\n      distance={props.distance ?? 0}\n      decay={props.decay ?? 2}\n      castShadow={props.castShadow ?? false}\n    />\n  ),\n\n  SpotLight: ({ props }: BaseComponentProps<ThreeProps<\"SpotLight\">>) => (\n    <spotLight\n      position={pos(props.position)}\n      color={props.color ?? \"#ffffff\"}\n      intensity={props.intensity ?? 1}\n      distance={props.distance ?? 0}\n      decay={props.decay ?? 2}\n      angle={props.angle ?? Math.PI / 3}\n      penumbra={props.penumbra ?? 0}\n      castShadow={props.castShadow ?? false}\n    />\n  ),\n\n  // ── Container ──────────────────────────────────────────────────────────\n\n  Group: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Group\">> & { children?: ReactNode }) => (\n    <group\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n    >\n      {children}\n    </group>\n  ),\n\n  // ── Model ──────────────────────────────────────────────────────────────\n\n  Model: ({ props }: BaseComponentProps<ThreeProps<\"Model\">>) => (\n    <Suspense fallback={null}>\n      <Gltf\n        src={props.url}\n        position={pos(props.position)}\n        rotation={rot(props.rotation)}\n        scale={scl(props.scale)}\n        castShadow={props.castShadow ?? false}\n        receiveShadow={props.receiveShadow ?? false}\n      />\n    </Suspense>\n  ),\n\n  // ── Environment ────────────────────────────────────────────────────────\n\n  Environment: ({ props }: BaseComponentProps<ThreeProps<\"Environment\">>) => (\n    <DreiEnvironment\n      preset={props.preset ?? \"sunset\"}\n      background={props.background ?? false}\n      blur={props.blur ?? 0}\n      environmentIntensity={props.intensity ?? 1}\n    />\n  ),\n\n  Fog: ({ props }: BaseComponentProps<ThreeProps<\"Fog\">>) => (\n    <fog\n      attach=\"fog\"\n      args={[props.color ?? \"#cccccc\", props.near ?? 10, props.far ?? 50]}\n    />\n  ),\n\n  GridHelper: ({ props }: BaseComponentProps<ThreeProps<\"GridHelper\">>) => (\n    <group\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n    >\n      <gridHelper\n        args={[\n          props.size ?? 10,\n          props.divisions ?? 10,\n          props.color ?? \"#888888\",\n          props.secondaryColor ?? \"#444444\",\n        ]}\n      />\n    </group>\n  ),\n\n  // ── Text ───────────────────────────────────────────────────────────────\n\n  Text3D: ({ props }: BaseComponentProps<ThreeProps<\"Text3D\">>) => (\n    <DreiText\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      fontSize={props.fontSize ?? 1}\n      color={props.color ?? \"#ffffff\"}\n      anchorX={props.anchorX ?? \"center\"}\n      anchorY={props.anchorY ?? \"middle\"}\n      maxWidth={props.maxWidth ?? undefined}\n    >\n      {props.text}\n    </DreiText>\n  ),\n\n  // ── Effects / Atmosphere ──────────────────────────────────────────────\n\n  Sparkles: ({ props }: BaseComponentProps<ThreeProps<\"Sparkles\">>) => (\n    <DreiSparkles\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      count={props.count ?? 50}\n      speed={props.speed ?? 0.4}\n      opacity={props.opacity ?? 1}\n      color={props.color ?? \"#ffffff\"}\n      size={props.size ?? 1}\n      noise={props.noise ?? 1}\n    />\n  ),\n\n  Stars: ({ props }: BaseComponentProps<ThreeProps<\"Stars\">>) => (\n    <DreiStars\n      radius={props.radius ?? 100}\n      depth={props.depth ?? 50}\n      count={props.count ?? 5000}\n      factor={props.factor ?? 4}\n      saturation={props.saturation ?? 0}\n      fade={props.fade ?? true}\n      speed={props.speed ?? 1}\n    />\n  ),\n\n  Sky: ({ props }: BaseComponentProps<ThreeProps<\"Sky\">>) => (\n    <DreiSky\n      distance={props.distance ?? 450000}\n      sunPosition={props.sunPosition ?? [0, 1, 0]}\n      inclination={props.inclination ?? undefined}\n      azimuth={props.azimuth ?? undefined}\n      mieCoefficient={props.mieCoefficient ?? 0.005}\n      mieDirectionalG={props.mieDirectionalG ?? 0.8}\n      rayleigh={props.rayleigh ?? 0.5}\n      turbidity={props.turbidity ?? 10}\n    />\n  ),\n\n  Cloud: ({ props }: BaseComponentProps<ThreeProps<\"Cloud\">>) => (\n    <DreiClouds limit={200}>\n      <DreiCloud\n        position={pos(props.position)}\n        seed={props.seed ?? undefined}\n        segments={props.segments ?? 20}\n        bounds={props.bounds ?? [10, 2, 10]}\n        volume={props.volume ?? 6}\n        speed={props.speed ?? 0.2}\n        fade={props.fade ?? 30}\n        opacity={props.opacity ?? 0.6}\n        color={props.color ?? \"#ffffff\"}\n        growth={props.growth ?? 4}\n      />\n    </DreiClouds>\n  ),\n\n  // ── Special Materials ───────────────────────────────────────────────\n\n  GlassSphere: ({ props }: BaseComponentProps<ThreeProps<\"GlassSphere\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <sphereGeometry\n        args={[\n          props.radius ?? 1,\n          props.widthSegments ?? 64,\n          props.heightSegments ?? 32,\n        ]}\n      />\n      <MeshTransmissionMaterial\n        color={props.color ?? \"#ffffff\"}\n        transmission={props.transmission ?? 1}\n        thickness={props.thickness ?? 0.5}\n        roughness={props.roughness ?? 0}\n        chromaticAberration={props.chromaticAberration ?? 0.06}\n        ior={props.ior ?? 1.5}\n        distortion={props.distortion ?? 0}\n        distortionScale={props.distortionScale ?? 0.3}\n        temporalDistortion={props.temporalDistortion ?? 0.5}\n        samples={props.samples ?? 10}\n        resolution={props.resolution ?? 256}\n        backside\n      />\n    </mesh>\n  ),\n\n  GlassBox: ({ props }: BaseComponentProps<ThreeProps<\"GlassBox\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <boxGeometry\n        args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}\n      />\n      <MeshTransmissionMaterial\n        color={props.color ?? \"#ffffff\"}\n        transmission={props.transmission ?? 1}\n        thickness={props.thickness ?? 0.5}\n        roughness={props.roughness ?? 0}\n        chromaticAberration={props.chromaticAberration ?? 0.06}\n        ior={props.ior ?? 1.5}\n        distortion={props.distortion ?? 0}\n        distortionScale={props.distortionScale ?? 0.3}\n        temporalDistortion={props.temporalDistortion ?? 0.5}\n        samples={props.samples ?? 10}\n        resolution={props.resolution ?? 256}\n        backside\n      />\n    </mesh>\n  ),\n\n  DistortSphere: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"DistortSphere\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <sphereGeometry\n        args={[\n          props.radius ?? 1,\n          props.widthSegments ?? 64,\n          props.heightSegments ?? 32,\n        ]}\n      />\n      <MeshDistortMaterial\n        color={props.color ?? \"#ff6600\"}\n        speed={props.speed ?? 2}\n        distort={props.distort ?? 0.5}\n        metalness={props.metalness ?? 0}\n        roughness={props.roughness ?? 0.2}\n      />\n    </mesh>\n  ),\n\n  // ── Extended Geometry ───────────────────────────────────────────────\n\n  TorusKnot: ({ props }: BaseComponentProps<ThreeProps<\"TorusKnot\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n    >\n      <torusKnotGeometry\n        args={[\n          props.radius ?? 1,\n          props.tube ?? 0.4,\n          props.tubularSegments ?? 64,\n          props.radialSegments ?? 8,\n          props.p ?? 2,\n          props.q ?? 3,\n        ]}\n      />\n      <MaterialComponent material={props.material} />\n    </mesh>\n  ),\n\n  RoundedBox: ({ props }: BaseComponentProps<ThreeProps<\"RoundedBox\">>) => (\n    <DreiRoundedBox\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      castShadow={props.castShadow ?? false}\n      receiveShadow={props.receiveShadow ?? false}\n      args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}\n      radius={props.radius ?? 0.05}\n      smoothness={props.smoothness ?? 4}\n    >\n      <MaterialComponent material={props.material} />\n    </DreiRoundedBox>\n  ),\n\n  // ── Shadows / Staging ───────────────────────────────────────────────\n\n  ContactShadows: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"ContactShadows\">>) => (\n    <DreiContactShadows\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      opacity={props.opacity ?? 0.5}\n      width={props.width ?? 10}\n      height={props.height ?? 10}\n      blur={props.blur ?? 2}\n      near={props.near ?? undefined}\n      far={props.far ?? 10}\n      smooth={props.smooth ?? true}\n      resolution={props.resolution ?? 256}\n      frames={props.frames ?? undefined}\n      color={props.color ?? \"#000000\"}\n    />\n  ),\n\n  Float: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Float\">> & { children?: ReactNode }) => (\n    <DreiFloat\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      speed={props.speed ?? 1}\n      rotationIntensity={props.rotationIntensity ?? 1}\n      floatIntensity={props.floatIntensity ?? 1}\n      enabled={props.enabled ?? true}\n    >\n      {children}\n    </DreiFloat>\n  ),\n\n  ReflectorPlane: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"ReflectorPlane\">>) => (\n    <mesh\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n    >\n      <planeGeometry args={[props.width ?? 10, props.height ?? 10]} />\n      <MeshReflectorMaterial\n        color={props.color ?? \"#888888\"}\n        resolution={props.resolution ?? 1024}\n        blur={props.blur ? [props.blur, props.blur] : [300, 100]}\n        mirror={props.mirror ?? 0.5}\n        mixBlur={props.mixBlur ?? 10}\n        mixStrength={props.mixStrength ?? 2}\n        depthScale={props.depthScale ?? 0.1}\n        metalness={props.metalness ?? 0.5}\n        roughness={props.roughness ?? 1}\n      />\n    </mesh>\n  ),\n\n  Backdrop: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Backdrop\">> & { children?: ReactNode }) => (\n    <DreiBackdrop\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      scale={scl(props.scale)}\n      floor={props.floor ?? 0.25}\n      segments={props.segments ?? 20}\n      receiveShadow={props.receiveShadow ?? true}\n    >\n      {children}\n    </DreiBackdrop>\n  ),\n\n  // ── Crazy / Magic ─────────────────────────────────────────────────────\n\n  MeshPortalMaterial: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"MeshPortalMaterial\">> & {\n    children?: ReactNode;\n  }) => (\n    <DreiMeshPortalMaterial\n      blend={props.blend ?? 0}\n      blur={props.blur ?? 0}\n      resolution={props.resolution ?? 512}\n    >\n      {children}\n    </DreiMeshPortalMaterial>\n  ),\n\n  HtmlLabel: ({ props }: BaseComponentProps<ThreeProps<\"HtmlLabel\">>) => (\n    <DreiHtml\n      position={pos(props.position)}\n      transform={props.transform ?? true}\n      distanceFactor={props.distanceFactor ?? 10}\n      center={props.center ?? true}\n      style={{\n        color: props.color ?? \"#ffffff\",\n        fontSize: props.fontSize ? `${props.fontSize}px` : \"16px\",\n        fontFamily: \"system-ui, sans-serif\",\n        whiteSpace: \"nowrap\",\n        pointerEvents: \"none\",\n      }}\n    >\n      {props.text}\n    </DreiHtml>\n  ),\n\n  // ── Post-Processing ────────────────────────────────────────────────────\n\n  EffectComposer: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"EffectComposer\">> & {\n    children?: ReactNode;\n  }) => {\n    if (props.enabled === false) return null;\n    return (\n      <postprocessing.EffectComposer multisampling={props.multisampling ?? 8}>\n        {children}\n      </postprocessing.EffectComposer>\n    );\n  },\n\n  Bloom: ({ props }: BaseComponentProps<ThreeProps<\"Bloom\">>) => (\n    <postprocessing.Bloom\n      intensity={props.intensity ?? 1.5}\n      luminanceThreshold={props.luminanceThreshold ?? 0.1}\n      luminanceSmoothing={props.luminanceSmoothing ?? 0.025}\n      mipmapBlur={props.mipmapBlur ?? true}\n    />\n  ),\n\n  Glitch: ({ props }: BaseComponentProps<ThreeProps<\"Glitch\">>) => {\n    const delay = props.delay ? new THREE.Vector2(...props.delay) : undefined;\n    const duration = props.duration\n      ? new THREE.Vector2(...props.duration)\n      : undefined;\n    const strength = props.strength\n      ? new THREE.Vector2(...props.strength)\n      : undefined;\n    return (\n      <postprocessing.Glitch\n        delay={delay}\n        duration={duration}\n        strength={strength}\n        active={props.active ?? true}\n        ratio={props.ratio ?? 0.85}\n      />\n    );\n  },\n\n  Vignette: ({ props }: BaseComponentProps<ThreeProps<\"Vignette\">>) => (\n    <postprocessing.Vignette\n      offset={props.offset ?? 0.5}\n      darkness={props.darkness ?? 0.5}\n    />\n  ),\n\n  // ── Animation Wrappers ──────────────────────────────────────────────\n\n  WarpTunnel: (() => {\n    function TunnelRing({\n      index,\n      ringCount,\n      radius,\n      length,\n      speed,\n      tubeRadius,\n      color,\n    }: {\n      index: number;\n      ringCount: number;\n      radius: number;\n      length: number;\n      speed: number;\n      tubeRadius: number;\n      color: THREE.Color;\n    }) {\n      const meshRef = useRef<THREE.Mesh>(null);\n      const spacing = length / ringCount;\n\n      useFrame((state) => {\n        if (!meshRef.current) return;\n        const t = state.clock.elapsedTime;\n        const z =\n          ((((-index * spacing + t * speed) % length) + length) % length) -\n          length;\n        meshRef.current.position.z = z;\n        const distFactor = 1 - Math.abs(z) / length;\n        const s = 0.3 + distFactor * 0.7;\n        meshRef.current.scale.set(s, s, s);\n      });\n\n      return (\n        <mesh ref={meshRef} rotation={[0, 0, (index * Math.PI) / 12]}>\n          <torusGeometry args={[radius, tubeRadius, 16, 64]} />\n          <meshStandardMaterial\n            color={color}\n            emissive={color}\n            emissiveIntensity={3}\n            transparent\n            opacity={0.7}\n            side={THREE.DoubleSide}\n          />\n        </mesh>\n      );\n    }\n\n    return ({ props }: BaseComponentProps<ThreeProps<\"WarpTunnel\">>) => {\n      const ringCount = props.ringCount ?? 80;\n      const radius = props.radius ?? 3;\n      const length = props.length ?? 40;\n      const speed = props.speed ?? 8;\n      const tubeRadius = props.tubeRadius ?? 0.02;\n      const color1 = props.color1 ?? \"#00ffff\";\n      const color2 = props.color2 ?? \"#ff00ff\";\n\n      const colors = useMemo(() => {\n        const c1 = new THREE.Color(color1);\n        const c2 = new THREE.Color(color2);\n        return Array.from({ length: ringCount }, (_, i) =>\n          new THREE.Color().lerpColors(c1, c2, i / ringCount),\n        );\n      }, [ringCount, color1, color2]);\n\n      return (\n        <group\n          position={pos(props.position)}\n          rotation={rot(props.rotation)}\n          scale={scl(props.scale)}\n        >\n          {colors.map((color, i) => (\n            <TunnelRing\n              key={i}\n              index={i}\n              ringCount={ringCount}\n              radius={radius}\n              length={length}\n              speed={speed}\n              tubeRadius={tubeRadius}\n              color={color}\n            />\n          ))}\n        </group>\n      );\n    };\n  })(),\n\n  Spin: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Spin\">> & { children?: ReactNode }) => {\n    const ref = useRef<THREE.Group>(null);\n    const speed = props.speed ?? 1;\n    const axis = props.axis ?? \"y\";\n\n    useFrame((_, delta) => {\n      if (!ref.current) return;\n      if (axis === \"x\") ref.current.rotation.x += delta * speed;\n      else if (axis === \"z\") ref.current.rotation.z += delta * speed;\n      else ref.current.rotation.y += delta * speed;\n    });\n\n    return (\n      <group\n        ref={ref}\n        position={pos(props.position)}\n        rotation={rot(props.rotation)}\n        scale={scl(props.scale)}\n      >\n        {children}\n      </group>\n    );\n  },\n\n  Orbit: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Orbit\">> & { children?: ReactNode }) => {\n    const ref = useRef<THREE.Group>(null);\n    const speed = props.speed ?? 1;\n    const radius = props.radius ?? 3;\n    const tilt = props.tilt ?? 0;\n    const baseY = props.position?.[1] ?? 0;\n\n    useFrame((state) => {\n      if (!ref.current) return;\n      const t = state.clock.elapsedTime * speed;\n      ref.current.position.x = Math.cos(t) * radius;\n      ref.current.position.z = Math.sin(t) * radius;\n      ref.current.position.y = baseY + Math.sin(t * 0.7) * tilt;\n    });\n\n    return <group ref={ref}>{children}</group>;\n  },\n\n  Pulse: ({\n    props,\n    children,\n  }: BaseComponentProps<ThreeProps<\"Pulse\">> & { children?: ReactNode }) => {\n    const ref = useRef<THREE.Group>(null);\n    const speed = props.speed ?? 1;\n    const min = props.min ?? 0.8;\n    const max = props.max ?? 1.2;\n\n    useFrame((state) => {\n      if (!ref.current) return;\n      const t = (Math.sin(state.clock.elapsedTime * speed) + 1) / 2;\n      const s = min + t * (max - min);\n      ref.current.scale.setScalar(s);\n    });\n\n    return (\n      <group\n        ref={ref}\n        position={pos(props.position)}\n        rotation={rot(props.rotation)}\n      >\n        {children}\n      </group>\n    );\n  },\n\n  CameraShake: ({ props }: BaseComponentProps<ThreeProps<\"CameraShake\">>) => (\n    <DreiCameraShake\n      intensity={props.intensity ?? 0.5}\n      maxYaw={props.maxYaw ?? 0.1}\n      maxPitch={props.maxPitch ?? 0.1}\n      maxRoll={props.maxRoll ?? 0.1}\n    />\n  ),\n\n  // ── Camera / Controls ──────────────────────────────────────────────\n\n  PerspectiveCamera: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"PerspectiveCamera\">>) => (\n    <DreiPerspectiveCamera\n      position={pos(props.position)}\n      rotation={rot(props.rotation)}\n      fov={props.fov ?? 50}\n      near={props.near ?? 0.1}\n      far={props.far ?? 1000}\n      makeDefault={props.makeDefault ?? false}\n    />\n  ),\n\n  OrbitControls: ({\n    props,\n  }: BaseComponentProps<ThreeProps<\"OrbitControls\">>) => (\n    <DreiOrbitControls\n      enableDamping={props.enableDamping ?? true}\n      dampingFactor={props.dampingFactor ?? 0.05}\n      enableZoom={props.enableZoom ?? true}\n      enablePan={props.enablePan ?? true}\n      enableRotate={props.enableRotate ?? true}\n      minDistance={props.minDistance ?? undefined}\n      maxDistance={props.maxDistance ?? undefined}\n      minPolarAngle={props.minPolarAngle ?? undefined}\n      maxPolarAngle={props.maxPolarAngle ?? undefined}\n      autoRotate={props.autoRotate ?? false}\n      autoRotateSpeed={props.autoRotateSpeed ?? 2}\n      target={props.target ?? undefined}\n    />\n  ),\n};\n"
  },
  {
    "path": "packages/react-three-fiber/src/index.ts",
    "content": "// Component implementations\nexport { threeComponents } from \"./components\";\n\n// Catalog definitions (also available via @json-render/react-three-fiber/catalog)\nexport {\n  threeComponentDefinitions,\n  type ComponentDefinition,\n  type ThreeProps,\n} from \"./catalog\";\n\n// Shared schemas\nexport {\n  vector3Schema,\n  materialSchema,\n  transformProps,\n  shadowProps,\n} from \"./schemas\";\n\n// Renderer\nexport {\n  ThreeRenderer,\n  ThreeCanvas,\n  type ThreeRendererProps,\n  type ThreeCanvasProps,\n} from \"./renderer\";\n"
  },
  {
    "path": "packages/react-three-fiber/src/r3f-jsx.d.ts",
    "content": "import type { ThreeElements } from \"@react-three/fiber\";\n\ndeclare module \"react/jsx-runtime\" {\n  namespace JSX {\n    interface IntrinsicElements extends ThreeElements {}\n  }\n}\n\ndeclare module \"react/jsx-dev-runtime\" {\n  namespace JSX {\n    interface IntrinsicElements extends ThreeElements {}\n  }\n}\n\ndeclare module \"react\" {\n  namespace JSX {\n    interface IntrinsicElements extends ThreeElements {}\n  }\n}\n"
  },
  {
    "path": "packages/react-three-fiber/src/renderer.tsx",
    "content": "import { type ReactNode } from \"react\";\nimport { Canvas, type CanvasProps } from \"@react-three/fiber\";\nimport {\n  Renderer,\n  StateProvider,\n  VisibilityProvider,\n  ActionProvider,\n  ValidationProvider,\n  type ComponentRegistry,\n  type ComponentRenderer,\n} from \"@json-render/react\";\nimport type { Spec, StateStore, ComputedFunction } from \"@json-render/core\";\n\n// =============================================================================\n// ThreeRenderer\n// =============================================================================\n\nexport interface ThreeRendererProps {\n  /** The spec to render as a 3D scene */\n  spec: Spec | null;\n  /** Component registry */\n  registry: ComponentRegistry;\n  /**\n   * External store (controlled mode). When provided, `initialState` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** Initial state model (uncontrolled mode) */\n  initialState?: Record<string, unknown>;\n  /** Action handlers */\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n  /** Additional R3F elements to render alongside the spec (lights, helpers, etc.) */\n  children?: ReactNode;\n}\n\n/**\n * Renders a json-render spec as a React Three Fiber scene graph.\n *\n * Place inside a `<Canvas>` or use `<ThreeCanvas>` for a convenience wrapper.\n *\n * @example\n * ```tsx\n * <Canvas shadows>\n *   <ThreeRenderer spec={spec} registry={registry} />\n * </Canvas>\n * ```\n */\nexport function ThreeRenderer({\n  spec,\n  registry,\n  store,\n  initialState,\n  handlers,\n  functions,\n  onStateChange,\n  loading,\n  fallback,\n  children,\n}: ThreeRendererProps) {\n  return (\n    <StateProvider\n      store={store}\n      initialState={initialState ?? spec?.state}\n      onStateChange={onStateChange}\n    >\n      <VisibilityProvider>\n        <ValidationProvider>\n          <ActionProvider handlers={handlers}>\n            <Renderer\n              spec={spec}\n              registry={registry}\n              loading={loading}\n              fallback={fallback}\n            />\n            {children}\n          </ActionProvider>\n        </ValidationProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n// =============================================================================\n// ThreeCanvas\n// =============================================================================\n\nexport interface ThreeCanvasProps extends ThreeRendererProps {\n  /** Enable shadows on the Canvas */\n  shadows?: CanvasProps[\"shadows\"];\n  /** Canvas camera config */\n  camera?: CanvasProps[\"camera\"];\n  /** Canvas className */\n  className?: string;\n  /** Canvas style */\n  style?: React.CSSProperties;\n  /** Additional Canvas props */\n  canvasProps?: Omit<CanvasProps, \"shadows\" | \"camera\" | \"className\" | \"style\">;\n}\n\n/**\n * Convenience wrapper that sets up a `<Canvas>` with a `<ThreeRenderer>` inside.\n *\n * @example\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 */\nexport function ThreeCanvas({\n  shadows,\n  camera,\n  className,\n  style,\n  canvasProps,\n  ...rendererProps\n}: ThreeCanvasProps) {\n  return (\n    <Canvas\n      shadows={shadows}\n      camera={camera}\n      className={className}\n      style={style}\n      {...canvasProps}\n    >\n      <ThreeRenderer {...rendererProps} />\n    </Canvas>\n  );\n}\n"
  },
  {
    "path": "packages/react-three-fiber/src/schemas.ts",
    "content": "import { z } from \"zod\";\n\nexport const vector3Schema = z.tuple([z.number(), z.number(), z.number()]);\n\nexport const materialSchema = z.object({\n  color: z.string().nullable(),\n  metalness: z.number().nullable(),\n  roughness: z.number().nullable(),\n  emissive: z.string().nullable(),\n  emissiveIntensity: z.number().nullable(),\n  opacity: z.number().nullable(),\n  transparent: z.boolean().nullable(),\n  wireframe: z.boolean().nullable(),\n});\n\nexport const transformProps = {\n  position: vector3Schema.nullable(),\n  rotation: vector3Schema.nullable(),\n  scale: vector3Schema.nullable(),\n} as const;\n\nexport const shadowProps = {\n  castShadow: z.boolean().nullable(),\n  receiveShadow: z.boolean().nullable(),\n} as const;\n"
  },
  {
    "path": "packages/react-three-fiber/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/react-three-fiber/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/catalog.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"react\",\n    \"react-dom\",\n    \"@json-render/core\",\n    \"@json-render/react\",\n    \"@react-three/fiber\",\n    \"@react-three/drei\",\n    \"three\",\n    \"zod\",\n  ],\n});\n"
  },
  {
    "path": "packages/redux/CHANGELOG.md",
    "content": "# @json-render/redux\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n"
  },
  {
    "path": "packages/redux/README.md",
    "content": "# @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```ts\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#### Options\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. For a simple store where the entire state is the model, use `(s) => s`. |\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, so you can dispatch any action shape your reducers expect.\n"
  },
  {
    "path": "packages/redux/package.json",
    "content": "{\n  \"name\": \"@json-render/redux\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Redux adapter for json-render StateStore\",\n  \"keywords\": [\n    \"json-render\",\n    \"redux\",\n    \"state-management\",\n    \"adapter\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/redux\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"peerDependencies\": {\n    \"@reduxjs/toolkit\": \">=2.0.0\",\n    \"redux\": \">=5.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@reduxjs/toolkit\": {\n      \"optional\": true\n    },\n    \"redux\": {\n      \"optional\": true\n    }\n  },\n  \"devDependencies\": {\n    \"@reduxjs/toolkit\": \"^2.11.2\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"redux\": \"^5.0.1\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "packages/redux/src/index.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { reduxStateStore } from \"./index\";\n\nfunction createTestStore(initial: Record<string, unknown> = {}) {\n  const uiSlice = createSlice({\n    name: \"ui\",\n    initialState: initial as Record<string, unknown>,\n    reducers: {\n      replace: (_state, action) => action.payload,\n    },\n  });\n\n  const reduxStore = configureStore({\n    reducer: { ui: uiSlice.reducer },\n  });\n\n  const store = reduxStateStore({\n    store: reduxStore,\n    selector: (state) => state.ui as Record<string, unknown>,\n    dispatch: (next, s) => s.dispatch(uiSlice.actions.replace(next)),\n  });\n\n  return { reduxStore, store, uiSlice };\n}\n\ndescribe(\"reduxStateStore\", () => {\n  it(\"get/set round-trip\", () => {\n    const { store } = createTestStore({ count: 0 });\n\n    expect(store.get(\"/count\")).toBe(0);\n\n    store.set(\"/count\", 42);\n\n    expect(store.get(\"/count\")).toBe(42);\n    expect(store.getSnapshot().count).toBe(42);\n  });\n\n  it(\"update round-trip with multiple values\", () => {\n    const { store } = createTestStore({});\n\n    store.update({ \"/a\": 1, \"/b\": \"hello\" });\n\n    expect(store.get(\"/a\")).toBe(1);\n    expect(store.get(\"/b\")).toBe(\"hello\");\n    expect(store.getSnapshot()).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"subscribe fires on set\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"subscribe fires on update\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"unsubscribe stops notifications\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    const unsub = store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n    expect(listener).toHaveBeenCalledTimes(1);\n\n    unsub();\n    store.set(\"/x\", 2);\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"getSnapshot immutability -- previous snapshot is not mutated\", () => {\n    const { store } = createTestStore({ user: { name: \"Alice\", age: 30 } });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/user/name\", \"Bob\");\n    const snap2 = store.getSnapshot();\n\n    expect(snap1.user).toEqual({ name: \"Alice\", age: 30 });\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect(snap1.user).not.toBe(snap2.user);\n  });\n\n  it(\"structural sharing -- untouched branches keep references\", () => {\n    const { store } = createTestStore({\n      a: { x: 1 },\n      b: { y: 2 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/a/x\", 99);\n    const snap2 = store.getSnapshot();\n\n    expect(snap2.b).toBe(snap1.b);\n    expect(snap2.a).not.toBe(snap1.a);\n  });\n\n  it(\"getServerSnapshot returns same as getSnapshot\", () => {\n    const { store } = createTestStore({ x: 1 });\n\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n\n    store.set(\"/x\", 2);\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n  });\n\n  it(\"subscribe does NOT fire when unrelated slice changes\", () => {\n    const uiSlice = createSlice({\n      name: \"ui\",\n      initialState: { count: 0 } as Record<string, unknown>,\n      reducers: {\n        replace: (_state, action) => action.payload,\n      },\n    });\n\n    const otherSlice = createSlice({\n      name: \"other\",\n      initialState: { value: \"a\" },\n      reducers: {\n        update: (state, action) => {\n          state.value = action.payload;\n        },\n      },\n    });\n\n    const reduxStore = configureStore({\n      reducer: {\n        ui: uiSlice.reducer,\n        other: otherSlice.reducer,\n      },\n    });\n\n    const store = reduxStateStore({\n      store: reduxStore,\n      selector: (state) => state.ui as Record<string, unknown>,\n      dispatch: (next, s) => s.dispatch(uiSlice.actions.replace(next)),\n    });\n\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    reduxStore.dispatch(otherSlice.actions.update(\"b\"));\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.get(\"/count\")).toBe(0);\n  });\n\n  it(\"set skips dispatch when value is unchanged\", () => {\n    const { store } = createTestStore({ x: 1 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"update skips dispatch when no values changed\", () => {\n    const { store } = createTestStore({ a: 1, b: 2 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"works with identity selector (entire state is the model)\", () => {\n    const slice = createSlice({\n      name: \"root\",\n      initialState: { x: 1 } as Record<string, unknown>,\n      reducers: {\n        replace: (_state, action) => action.payload,\n      },\n    });\n\n    const reduxStore = configureStore({\n      reducer: slice.reducer,\n    });\n\n    const store = reduxStateStore({\n      store: reduxStore,\n      selector: (s) => s as Record<string, unknown>,\n      dispatch: (next, s) => s.dispatch(slice.actions.replace(next)),\n    });\n\n    expect(store.get(\"/x\")).toBe(1);\n\n    store.set(\"/x\", 2);\n    expect(store.get(\"/x\")).toBe(2);\n  });\n\n  it(\"defaults selector to identity when omitted\", () => {\n    const slice = createSlice({\n      name: \"root\",\n      initialState: { x: 1 } as Record<string, unknown>,\n      reducers: {\n        replace: (_state, action) => action.payload,\n      },\n    });\n\n    const reduxStore = configureStore({\n      reducer: slice.reducer,\n    });\n\n    const store = reduxStateStore({\n      store: reduxStore,\n      dispatch: (next, s) => s.dispatch(slice.actions.replace(next)),\n    });\n\n    expect(store.get(\"/x\")).toBe(1);\n\n    store.set(\"/x\", 2);\n    expect(store.get(\"/x\")).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/redux/src/index.ts",
    "content": "import type { StateModel, StateStore } from \"@json-render/core\";\nimport { createStoreAdapter } from \"@json-render/core/store-utils\";\nimport type { Store, Action, UnknownAction } from \"redux\";\n\nexport type { StateStore } from \"@json-render/core\";\n\n/**\n * Options for {@link reduxStateStore}.\n */\nexport interface ReduxStateStoreOptions<\n  S extends StateModel = StateModel,\n  A extends Action = UnknownAction,\n> {\n  /** The Redux store instance. */\n  store: Store<S, A>;\n  /**\n   * Select the json-render state slice from the Redux state tree.\n   * Defaults to `(state) => state` (the entire store is the state model).\n   */\n  selector?: (state: S) => StateModel;\n  /**\n   * Dispatch a state change back to the Redux store.\n   *\n   * Called for every `set` / `update` with the full next state model.\n   * You must dispatch an action that replaces the selected slice.\n   *\n   * @example\n   * ```ts\n   * dispatch: (nextState, reduxStore) =>\n   *   reduxStore.dispatch(replaceState(nextState))\n   * ```\n   */\n  dispatch: (nextState: StateModel, store: Store<S, A>) => void;\n}\n\n/**\n * Create a {@link StateStore} backed by a Redux store.\n *\n * @example\n * ```ts\n * import { configureStore, createSlice } from \"@reduxjs/toolkit\";\n * import { reduxStateStore } from \"@json-render/redux\";\n *\n * const 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 * const reduxStore = configureStore({\n *   reducer: { ui: uiSlice.reducer },\n * });\n *\n * const store = reduxStateStore({\n *   store: reduxStore,\n *   selector: (state) => state.ui,\n *   dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),\n * });\n *\n * <StateProvider store={store}>...</StateProvider>\n * ```\n */\nexport function reduxStateStore<\n  S extends StateModel = StateModel,\n  A extends Action = UnknownAction,\n>(options: ReduxStateStoreOptions<S, A>): StateStore {\n  const { store, selector = (s: S) => s as StateModel, dispatch } = options;\n\n  return createStoreAdapter({\n    getSnapshot: () => selector(store.getState()),\n    setSnapshot: (next) => dispatch(next, store),\n    subscribe(listener) {\n      let prev = selector(store.getState());\n      return store.subscribe(() => {\n        const current = selector(store.getState());\n        if (current !== prev) {\n          prev = current;\n          listener();\n          // Re-read after listener in case it triggered a synchronous dispatch;\n          // absorb that change so it doesn't fire a duplicate notification.\n          prev = selector(store.getState());\n        }\n      });\n    },\n  });\n}\n"
  },
  {
    "path": "packages/redux/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/redux/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"@json-render/core\",\n    \"@json-render/core/store-utils\",\n    \"redux\",\n    \"@reduxjs/toolkit\",\n  ],\n});\n"
  },
  {
    "path": "packages/remotion/CHANGELOG.md",
    "content": "# @json-render/remotion\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n\n## 0.8.0\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n\n## 0.7.0\n\n### Patch Changes\n\n- Updated dependencies [2d70fab]\n  - @json-render/core@0.7.0\n\n## 0.6.1\n\n### Patch Changes\n\n- @json-render/core@0.6.1\n\n## 0.6.1\n\n### Patch Changes\n\n- Updated dependencies [ea97aff]\n  - @json-render/core@0.6.1\n\n## 0.6.0\n\n### Minor Changes\n\n- 06b8745: Chat mode (inline GenUI), AI SDK integration, two-way binding, and expression-based visibility/props.\n\n  ### New: Chat Mode (Inline GenUI)\n\n  Two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets AI respond conversationally with embedded UI specs — ideal for chatbots and copilot experiences.\n  - `catalog.prompt({ mode: \"chat\" })` generates a chat-aware system prompt\n  - `pipeJsonRender()` server-side transform separates text from JSONL patches in a mixed stream\n  - `createJsonRenderTransform()` low-level TransformStream for custom pipelines\n\n  ### New: AI SDK Integration\n\n  First-class Vercel AI SDK support with typed data parts and stream utilities.\n  - `SpecDataPart` type for `data-spec` stream parts (patch, flat, nested payloads)\n  - `SPEC_DATA_PART` / `SPEC_DATA_PART_TYPE` constants for type-safe part filtering\n  - `createMixedStreamParser()` for parsing mixed text + JSONL streams\n\n  ### New: React Chat Hooks\n  - `useChatUI()` — full chat hook with message history, streaming, and spec extraction\n  - `useJsonRenderMessage()` — extract spec + text from a message's parts array\n  - `buildSpecFromParts()` / `getTextFromParts()` — utilities for working with AI SDK message parts\n  - `useBoundProp()` — two-way binding hook for `$bindState` / `$bindItem` expressions\n\n  ### New: Two-Way Binding\n\n  Props can now use `$bindState` and `$bindItem` expressions for two-way data binding. The renderer resolves bindings and passes a `bindings` map to components, enabling write-back to state.\n\n  ### New: Expression-Based Props and Visibility\n\n  Replaced string token rewriting with structured expression objects:\n  - Props: `{ $state: \"/path\" }`, `{ $item: \"field\" }`, `{ $index: true }`\n  - Visibility: `{ $state: \"/path\", eq: \"value\" }`, `{ $item: \"active\" }`, `{ $index: true, gt: 0 }`\n  - Logic: `{ $and: [...] }`, `{ $or: [...] }`, and implicit AND via arrays\n  - Comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`\n\n  ### New: Utilities\n  - `applySpecPatch()` — typed convenience wrapper for applying a single patch to a Spec\n  - `nestedToFlat()` — convert nested tree specs to flat `{ root, elements }` format\n  - `resolveBindings()` / `resolveActionParam()` — resolve binding paths and action params\n\n  ### New: Chat Example\n\n  Full-featured chat example (`examples/chat`) with AI agent, tool calls (crypto, GitHub, Hacker News, weather, search), theme toggle, and streaming UI generation.\n\n  ### Improved: Renderer\n  - `ElementRenderer` is now `React.memo`'d for better performance in repeat lists\n  - `emit` is always defined (never `undefined`) — no more optional chaining needed\n  - Action params are resolved through `resolveActionParam` supporting `$item`, `$index`, `$state`\n  - Repeat scope now passes the actual item object instead of requiring token rewriting\n\n  ### Breaking Changes\n  - **Expressions renamed**: `{ $path }` / `{ path }` replaced by `{ $state }`, `{ $item }`, `{ $index }`\n  - **Visibility conditions**: `{ path }` → `{ $state }`, `{ and/or/not }` → `{ $and/$or }` with `not` as operator flag\n  - **DynamicValue**: `{ path: string }` → `{ $state: string }`\n  - **Repeat field**: `repeat.path` → `repeat.statePath`\n  - **Action params**: `path` → `statePath` in setState action params\n  - **Provider props**: `actionHandlers` → `handlers` on `JSONUIProvider`/`ActionProvider`\n  - **Auth removed**: `AuthState` type and `{ auth }` visibility conditions removed — model auth as regular state\n  - **Legacy catalog removed**: `createCatalog`, `generateCatalogPrompt`, `generateSystemPrompt`, `ComponentDefinition`, `CatalogConfig`, `SystemPromptOptions` removed\n  - **React exports removed**: `createRendererFromCatalog`, `rewriteRepeatTokens`\n  - **Codegen**: `traverseTree` → `traverseSpec`, `SpecVisitor` → `TreeVisitor`\n\n### Patch Changes\n\n- Updated dependencies [06b8745]\n  - @json-render/core@0.6.0\n\n## 0.5.2\n\n### Patch Changes\n\n- 429e456: Fix LLM hallucinations by dynamically generating prompt examples from the user's catalog instead of hardcoding component names. Adds optional `example` field to `ComponentDefinition` with Zod schema introspection fallback. Mentions RFC 6902 in output format section.\n- Updated dependencies [429e456]\n  - @json-render/core@0.5.2\n\n## 0.5.1\n\n### Patch Changes\n\n- d9a4efd: Prevent rendering errors from crashing the application. Added error boundaries to all renderers so a single bad component silently disappears instead of causing a white-screen-of-death. Fixed Select and Radio components to handle non-string option values from AI output.\n  - @json-render/core@0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- 3d2d1ad: Add @json-render/react-native package, event system (emit replaces onAction), repeat/list rendering, user prompt builder, spec validation, and rename DataProvider to StateProvider.\n\n### Patch Changes\n\n- Updated dependencies [3d2d1ad]\n  - @json-render/core@0.5.0\n\n## 0.4.4\n\n### Patch Changes\n\n- dd17549: remove key/parentKey from flat specs, RFC 6902 compliance for SpecStream\n- Updated dependencies [dd17549]\n  - @json-render/core@0.4.4\n\n## 0.4.3\n\n### Patch Changes\n\n- 61ee8e5: include remove op in system prompt\n- Updated dependencies [61ee8e5]\n  - @json-render/core@0.4.3\n\n## 0.4.2\n\n### Patch Changes\n\n- 54bce09: add defineRegistry function\n- Updated dependencies [54bce09]\n  - @json-render/core@0.4.2\n"
  },
  {
    "path": "packages/remotion/README.md",
    "content": "# @json-render/remotion\n\nRemotion video renderer for json-render. Turn JSON timeline specs into video compositions.\n\n## Installation\n\n```bash\nnpm install @json-render/remotion @json-render/core remotion @remotion/player zod\n```\n\n## Quick Start\n\n### 1. Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport {\n  schema,\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n} from \"@json-render/remotion\";\n\n// Use standard definitions or add your own\nexport const videoCatalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    // Add custom components here\n  },\n  transitions: standardTransitionDefinitions,\n  effects: standardEffectDefinitions,\n});\n```\n\n### 2. Render with the Player\n\n```tsx\nimport { Player } from \"@remotion/player\";\nimport { Renderer } from \"@json-render/remotion\";\n\nfunction VideoPlayer({ spec }) {\n  return (\n    <Player\n      component={Renderer}\n      inputProps={{ spec }}\n      durationInFrames={spec.composition.durationInFrames}\n      fps={spec.composition.fps}\n      compositionWidth={spec.composition.width}\n      compositionHeight={spec.composition.height}\n      controls\n    />\n  );\n}\n```\n\n## Timeline Spec Format\n\n```typescript\ninterface TimelineSpec {\n  composition: {\n    id: string;\n    fps: number;\n    width: number;\n    height: number;\n    durationInFrames: number;\n  };\n  tracks: Array<{\n    id: string;\n    name: string;\n    type: \"video\" | \"overlay\" | \"audio\";\n    enabled: boolean;\n  }>;\n  clips: Array<{\n    id: string;\n    trackId: string;\n    component: string;         // Component name from catalog\n    props: object;             // Component-specific props\n    from: number;              // Start frame\n    durationInFrames: number;\n    transitionIn?: {\n      type: string;\n      durationInFrames: number;\n    };\n    transitionOut?: {\n      type: string;\n      durationInFrames: number;\n    };\n  }>;\n  audio: {\n    tracks: Array<{\n      id: string;\n      src: string;\n      from: number;\n      durationInFrames: number;\n      volume?: number;\n    }>;\n  };\n}\n```\n\nExample spec:\n\n```json\n{\n  \"composition\": {\n    \"id\": \"intro-video\",\n    \"fps\": 30,\n    \"width\": 1920,\n    \"height\": 1080,\n    \"durationInFrames\": 300\n  },\n  \"tracks\": [\n    { \"id\": \"main\", \"name\": \"Main Video\", \"type\": \"video\", \"enabled\": true },\n    { \"id\": \"overlay\", \"name\": \"Overlays\", \"type\": \"overlay\", \"enabled\": true }\n  ],\n  \"clips\": [\n    {\n      \"id\": \"clip-1\",\n      \"trackId\": \"main\",\n      \"component\": \"TitleCard\",\n      \"props\": {\n        \"title\": \"Welcome\",\n        \"subtitle\": \"To the future\",\n        \"backgroundColor\": \"#1a1a1a\",\n        \"textColor\": \"#ffffff\"\n      },\n      \"from\": 0,\n      \"durationInFrames\": 90,\n      \"transitionIn\": { \"type\": \"fade\", \"durationInFrames\": 15 },\n      \"transitionOut\": { \"type\": \"fade\", \"durationInFrames\": 15 }\n    }\n  ],\n  \"audio\": { \"tracks\": [] }\n}\n```\n\n## Standard Components\n\nThe package includes pre-built video components:\n\n| Component | Description | Key Props |\n|-----------|-------------|-----------|\n| `TitleCard` | Full-screen title with subtitle | `title`, `subtitle`, `backgroundColor`, `textColor` |\n| `ImageSlide` | Full-screen image display | `src`, `alt`, `fit` |\n| `SplitScreen` | Two-column layout | `leftTitle`, `rightTitle`, `leftContent`, `rightContent` |\n| `QuoteCard` | Quote with attribution | `quote`, `author`, `role` |\n| `StatCard` | Large statistic display | `value`, `label`, `trend` |\n| `LowerThird` | Name/title overlay | `name`, `title` |\n| `TextOverlay` | Centered text | `text`, `fontSize`, `fontFamily` |\n| `TypingText` | Terminal typing effect | `text`, `charsPerSecond`, `showCursor` |\n| `LogoBug` | Corner logo overlay | `src`, `position`, `size` |\n| `VideoClip` | Video playback | `src` |\n\n## Standard Transitions\n\n| Transition | Description |\n|------------|-------------|\n| `fade` | Opacity fade in/out |\n| `slideLeft` | Slide from right |\n| `slideRight` | Slide from left |\n| `slideUp` | Slide from bottom |\n| `slideDown` | Slide from top |\n| `zoom` | Scale zoom in/out |\n| `wipe` | Horizontal wipe |\n\n## Custom Components\n\nAdd custom components to the Renderer:\n\n```tsx\nimport { Renderer, standardComponents, ClipWrapper } from \"@json-render/remotion\";\nimport type { Clip } from \"@json-render/remotion\";\n\n// Define a custom component\nfunction CustomOverlay({ clip }: { clip: Clip }) {\n  return (\n    <ClipWrapper clip={clip}>\n      <div style={{ backgroundColor: clip.props.color }}>\n        {clip.props.message}\n      </div>\n    </ClipWrapper>\n  );\n}\n\n// Merge with standard components\nconst customComponents = {\n  ...standardComponents,\n  CustomOverlay,\n};\n\n// Pass to Renderer\n<Player\n  component={Renderer}\n  inputProps={{ spec, components: customComponents }}\n  // ...\n/>\n```\n\n## Hooks and Utilities\n\n### useTransition\n\nCalculate transition styles for a clip:\n\n```tsx\nimport { useTransition } from \"@json-render/remotion\";\nimport { useCurrentFrame } from \"remotion\";\n\nfunction MyComponent({ clip }: { clip: Clip }) {\n  const frame = useCurrentFrame();\n  const transition = useTransition(clip, frame);\n\n  return (\n    <div style={{ opacity: transition.opacity }}>\n      Content\n    </div>\n  );\n}\n```\n\n### ClipWrapper\n\nApply transitions automatically:\n\n```tsx\nimport { ClipWrapper } from \"@json-render/remotion\";\n\nfunction MyComponent({ clip }: { clip: Clip }) {\n  return (\n    <ClipWrapper clip={clip}>\n      {/* Content gets transition styles applied */}\n      <div>My content</div>\n    </ClipWrapper>\n  );\n}\n```\n\n## Generate AI Prompts\n\n```typescript\nconst systemPrompt = videoCatalog.prompt();\n// Returns detailed prompt with:\n// - Component descriptions and props\n// - Transition types\n// - Effect definitions\n// - Timeline spec format requirements\n```\n\n## Exports\n\n### Components\n\n```typescript\nimport {\n  // Main renderer\n  Renderer,\n  standardComponents,\n\n  // Standard components\n  TitleCard,\n  ImageSlide,\n  SplitScreen,\n  QuoteCard,\n  StatCard,\n  LowerThird,\n  TextOverlay,\n  TypingText,\n  LogoBug,\n  VideoClip,\n\n  // Utilities\n  ClipWrapper,\n  useTransition,\n} from \"@json-render/remotion\";\n```\n\n### Types\n\n```typescript\nimport type {\n  Clip,\n  TimelineSpec,\n  AudioTrack,\n  TransitionStyles,\n  ClipComponent,\n  ComponentRegistry,\n} from \"@json-render/remotion\";\n```\n\n### Schema and Catalog Definitions\n\n```typescript\nimport {\n  schema,\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n} from \"@json-render/remotion\";\n```\n\n## Why Different from React?\n\n| Feature | @json-render/react | @json-render/remotion |\n|---------|-------------------|----------------------|\n| Spec Format | Element tree (nested components) | Timeline (tracks + clips) |\n| Components | UI components (Button, Card) | Video components (scenes, overlays) |\n| Timing | User interactions | Frame-based animations |\n| Output | React DOM | Remotion video |\n\njson-render is \"JSON to render\" - the spec format and components are completely flexible per renderer.\n"
  },
  {
    "path": "packages/remotion/package.json",
    "content": "{\n  \"name\": \"@json-render/remotion\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Remotion renderer for @json-render/core. JSON becomes video compositions.\",\n  \"keywords\": [\n    \"json\",\n    \"video\",\n    \"remotion\",\n    \"ai\",\n    \"generative-video\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"compositions\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/remotion\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./server\": {\n      \"types\": \"./dist/server.d.ts\",\n      \"import\": \"./dist/server.mjs\",\n      \"require\": \"./dist/server.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"remotion\": \"4.0.418\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.2.3\",\n    \"remotion\": \"^4.0.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/remotion/src/catalog-types.ts",
    "content": "import type { ReactNode } from \"react\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferComponentProps,\n} from \"@json-render/core\";\n\n// =============================================================================\n// Remotion-specific Types\n// =============================================================================\n\n/**\n * Frame information passed to video components\n */\nexport interface FrameContext {\n  /** Current frame number */\n  frame: number;\n  /** Frames per second */\n  fps: number;\n  /** Total duration in frames */\n  durationInFrames: number;\n  /** Width in pixels */\n  width: number;\n  /** Height in pixels */\n  height: number;\n}\n\n/**\n * Context passed to video component render functions\n */\nexport interface VideoComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> {\n  /** Component props from the spec */\n  props: InferComponentProps<C, K>;\n  /** Frame information */\n  frame: FrameContext;\n  /** Children (for container components) */\n  children?: ReactNode;\n}\n\n/**\n * Video component render function type\n *\n * @example\n * const TitleCard: VideoComponentFn<typeof catalog, 'TitleCard'> = ({ props, frame }) => {\n *   const opacity = interpolate(frame.frame, [0, 30], [0, 1]);\n *   return <div style={{ opacity }}>{props.title}</div>;\n * };\n */\nexport type VideoComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: VideoComponentContext<C, K>) => ReactNode;\n\n/**\n * Registry of all video component render functions for a catalog\n *\n * @example\n * const components: VideoComponents<typeof myCatalog> = {\n *   TitleCard: ({ props, frame }) => <TitleCard {...props} />,\n *   ImageSlide: ({ props }) => <Img src={props.src} />,\n * };\n */\nexport type VideoComponents<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: VideoComponentFn<C, K>;\n};\n\n// =============================================================================\n// Transition Types\n// =============================================================================\n\n/**\n * Transition render function\n * Returns a style object to apply during the transition\n */\nexport type TransitionFn = (progress: number) => React.CSSProperties;\n\n/**\n * Built-in transition types\n */\nexport type BuiltInTransition =\n  | \"fade\"\n  | \"slideLeft\"\n  | \"slideRight\"\n  | \"slideUp\"\n  | \"slideDown\"\n  | \"zoom\"\n  | \"wipe\";\n\n// =============================================================================\n// Effect Types\n// =============================================================================\n\n/**\n * Infer effect params from catalog\n */\ntype InferCatalogEffects<C extends Catalog> = C extends {\n  data: { effects: infer E };\n}\n  ? E\n  : never;\n\n/**\n * Effect handler function\n */\nexport type EffectFn<\n  C extends Catalog,\n  K extends keyof InferCatalogEffects<C>,\n> = InferCatalogEffects<C>[K] extends { params: { _output: infer P } }\n  ? (params: P, frame: FrameContext) => React.CSSProperties\n  : (params: undefined, frame: FrameContext) => React.CSSProperties;\n\n/**\n * Registry of all effect handlers for a catalog\n */\nexport type Effects<C extends Catalog> = {\n  [K in keyof InferCatalogEffects<C>]: EffectFn<C, K>;\n};\n"
  },
  {
    "path": "packages/remotion/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Standard component definitions for Remotion catalogs\n *\n * These can be used directly or extended with custom components.\n */\nexport const standardComponentDefinitions = {\n  // ==========================================================================\n  // Scene Components (full-screen)\n  // ==========================================================================\n\n  TitleCard: {\n    props: z.object({\n      title: z.string(),\n      subtitle: z.string().nullable(),\n      backgroundColor: z.string().nullable(),\n      textColor: z.string().nullable(),\n    }),\n    type: \"scene\",\n    defaultDuration: 90,\n    description:\n      \"Full-screen title card with centered text. Use for intros, outros, and section breaks.\",\n    example: { title: \"Welcome\", subtitle: \"An introduction\" },\n  },\n\n  ImageSlide: {\n    props: z.object({\n      src: z.string(),\n      alt: z.string(),\n      fit: z.enum([\"cover\", \"contain\"]).nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    type: \"image\",\n    defaultDuration: 150,\n    description:\n      \"Full-screen image display. Use for product shots, photos, and visual content.\",\n    example: {\n      src: \"https://picsum.photos/1920/1080?random=1\",\n      alt: \"Hero image\",\n      fit: \"cover\",\n    },\n  },\n\n  SplitScreen: {\n    props: z.object({\n      leftTitle: z.string(),\n      rightTitle: z.string(),\n      leftColor: z.string().nullable(),\n      rightColor: z.string().nullable(),\n    }),\n    type: \"scene\",\n    defaultDuration: 120,\n    description:\n      \"Split screen with two sides. Use for comparisons or before/after.\",\n  },\n\n  QuoteCard: {\n    props: z.object({\n      quote: z.string(),\n      author: z.string().nullable(),\n      backgroundColor: z.string().nullable(),\n      textColor: z.string().nullable(),\n      transparent: z.boolean().nullable(),\n    }),\n    type: \"scene\",\n    defaultDuration: 150,\n    description:\n      \"Quote display with author. Props: quote, author, textColor, backgroundColor. Set transparent:true when using as overlay on images.\",\n    example: {\n      quote: \"The best way to predict the future is to invent it.\",\n      author: \"Alan Kay\",\n    },\n  },\n\n  StatCard: {\n    props: z.object({\n      value: z.string(),\n      label: z.string(),\n      prefix: z.string().nullable(),\n      suffix: z.string().nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    type: \"scene\",\n    defaultDuration: 90,\n    description: \"Large statistic display. Use for key metrics and numbers.\",\n    example: { value: \"10M+\", label: \"Users worldwide\", prefix: \"\" },\n  },\n\n  TypingText: {\n    props: z.object({\n      text: z.string(),\n      backgroundColor: z.string().nullable(),\n      textColor: z.string().nullable(),\n      fontSize: z.number().nullable(),\n      fontFamily: z.enum([\"monospace\", \"sans-serif\", \"serif\"]).nullable(),\n      showCursor: z.boolean().nullable(),\n      cursorChar: z.string().nullable(),\n      charsPerSecond: z.number().nullable(),\n    }),\n    type: \"scene\",\n    defaultDuration: 180,\n    description:\n      \"Terminal-style typing animation that reveals text character by character. Perfect for code demos, CLI commands, and dramatic text reveals.\",\n  },\n\n  // ==========================================================================\n  // Overlay Components\n  // ==========================================================================\n\n  LowerThird: {\n    props: z.object({\n      name: z.string(),\n      title: z.string().nullable(),\n      backgroundColor: z.string().nullable(),\n    }),\n    type: \"overlay\",\n    defaultDuration: 120,\n    description:\n      \"Name/title overlay in lower third of screen. Use to identify speakers.\",\n  },\n\n  TextOverlay: {\n    props: z.object({\n      text: z.string(),\n      position: z.enum([\"top\", \"center\", \"bottom\"]).nullable(),\n      fontSize: z.enum([\"small\", \"medium\", \"large\"]).nullable(),\n    }),\n    type: \"overlay\",\n    defaultDuration: 90,\n    description: \"Simple text overlay. Use for captions and annotations.\",\n  },\n\n  LogoBug: {\n    props: z.object({\n      position: z\n        .enum([\"top-left\", \"top-right\", \"bottom-left\", \"bottom-right\"])\n        .nullable(),\n      opacity: z.number().nullable(),\n    }),\n    type: \"overlay\",\n    defaultDuration: 300,\n    description: \"Corner logo watermark. Use for branding throughout video.\",\n  },\n\n  // ==========================================================================\n  // Video Components\n  // ==========================================================================\n\n  VideoClip: {\n    props: z.object({\n      src: z.string(),\n      startFrom: z.number().nullable(),\n      volume: z.number().nullable(),\n    }),\n    type: \"video\",\n    defaultDuration: 150,\n    description: \"Video file playback. Use for B-roll and footage.\",\n  },\n};\n\n/**\n * Standard transition definitions for Remotion catalogs\n */\nexport const standardTransitionDefinitions = {\n  fade: {\n    defaultDuration: 15,\n    description: \"Smooth fade in/out. Use for gentle transitions.\",\n  },\n  slideLeft: {\n    defaultDuration: 20,\n    description: \"Slide from right to left. Use for forward progression.\",\n  },\n  slideRight: {\n    defaultDuration: 20,\n    description: \"Slide from left to right. Use for backward progression.\",\n  },\n  slideUp: {\n    defaultDuration: 15,\n    description: \"Slide from bottom to top. Use for overlays appearing.\",\n  },\n  slideDown: {\n    defaultDuration: 15,\n    description: \"Slide from top to bottom. Use for overlays disappearing.\",\n  },\n  zoom: {\n    defaultDuration: 20,\n    description: \"Zoom in/out effect. Use for emphasis.\",\n  },\n  wipe: {\n    defaultDuration: 15,\n    description: \"Horizontal wipe. Use for scene changes.\",\n  },\n  none: {\n    defaultDuration: 0,\n    description: \"No transition (hard cut).\",\n  },\n};\n\n/**\n * Standard effect definitions for Remotion catalogs\n */\nexport const standardEffectDefinitions = {\n  kenBurns: {\n    params: z.object({\n      startScale: z.number(),\n      endScale: z.number(),\n      panX: z.number().nullable(),\n      panY: z.number().nullable(),\n    }),\n    description: \"Ken Burns pan and zoom effect for images.\",\n  },\n  pulse: {\n    params: z.object({\n      intensity: z.number(),\n    }),\n    description: \"Subtle pulsing scale effect for emphasis.\",\n  },\n  shake: {\n    params: z.object({\n      intensity: z.number(),\n    }),\n    description: \"Camera shake effect for energy.\",\n  },\n};\n\n/**\n * Type for component definition\n */\nexport type ComponentDefinition = {\n  props: z.ZodType;\n  type: string;\n  defaultDuration: number;\n  description: string;\n};\n\n/**\n * Type for transition definition\n */\nexport type TransitionDefinition = {\n  defaultDuration: number;\n  description: string;\n};\n\n/**\n * Type for effect definition\n */\nexport type EffectDefinition = {\n  params: z.ZodType;\n  description: string;\n};\n"
  },
  {
    "path": "packages/remotion/src/components/ClipWrapper.tsx",
    "content": "\"use client\";\n\nimport { AbsoluteFill, useCurrentFrame } from \"remotion\";\nimport { useTransition, useMotion } from \"./hooks\";\nimport type { Clip } from \"./types\";\n\ninterface ClipWrapperProps {\n  clip: Clip;\n  children: React.ReactNode;\n}\n\n/**\n * Wrapper component that applies transition and motion animations to clips\n *\n * Automatically handles:\n * - transitionIn/transitionOut: basic transition presets (fade, slide, zoom, etc.)\n * - motion: declarative enter/exit/loop animations with spring physics\n *\n * Transitions and motion compose together:\n * - Opacity: multiplied (both affect final opacity)\n * - Transforms: added (both contribute to final position/scale/rotation)\n */\nexport function ClipWrapper({ clip, children }: ClipWrapperProps) {\n  const frame = useCurrentFrame();\n  const absoluteFrame = frame + clip.from;\n\n  // Get transition styles (from transitionIn/transitionOut)\n  const transition = useTransition(clip, absoluteFrame);\n\n  // Get motion styles (from motion.enter/exit/loop)\n  const motion = useMotion(clip, absoluteFrame);\n\n  // Compose styles: multiply opacity, add transforms\n  const composedOpacity = transition.opacity * motion.opacity;\n  const composedTranslateX = transition.translateX + motion.translateX;\n  const composedTranslateY = transition.translateY + motion.translateY;\n  const composedScale = transition.scale * motion.scale;\n  const composedRotate = motion.rotate; // Only from motion (transitions don't rotate)\n\n  return (\n    <AbsoluteFill\n      style={{\n        opacity: composedOpacity,\n        transform: `translateX(${composedTranslateX}%) translateY(${composedTranslateY}%) scale(${composedScale}) rotate(${composedRotate}deg)`,\n      }}\n    >\n      {children}\n    </AbsoluteFill>\n  );\n}\n"
  },
  {
    "path": "packages/remotion/src/components/Renderer.tsx",
    "content": "\"use client\";\n\nimport React, { type ErrorInfo, type ReactNode } from \"react\";\nimport { AbsoluteFill, Sequence } from \"remotion\";\nimport type { TimelineSpec, ComponentRegistry, Clip } from \"./types\";\n\n// Import standard components\nimport {\n  TitleCard,\n  ImageSlide,\n  SplitScreen,\n  QuoteCard,\n  StatCard,\n  LowerThird,\n  TextOverlay,\n  TypingText,\n  LogoBug,\n  VideoClip,\n} from \"./standard\";\n\n/**\n * Standard components provided by @json-render/remotion\n */\nexport const standardComponents: ComponentRegistry = {\n  TitleCard,\n  ImageSlide,\n  SplitScreen,\n  QuoteCard,\n  StatCard,\n  LowerThird,\n  TextOverlay,\n  TypingText,\n  LogoBug,\n  VideoClip,\n};\n\n// ---------------------------------------------------------------------------\n// ClipErrorBoundary – catches rendering errors in individual clips so\n// a single bad clip never crashes the entire composition.\n// ---------------------------------------------------------------------------\n\ninterface ClipErrorBoundaryProps {\n  clipId: string;\n  component: string;\n  children: ReactNode;\n}\n\ninterface ClipErrorBoundaryState {\n  hasError: boolean;\n}\n\nclass ClipErrorBoundary extends React.Component<\n  ClipErrorBoundaryProps,\n  ClipErrorBoundaryState\n> {\n  constructor(props: ClipErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ClipErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, info: React.ErrorInfo) {\n    console.error(\n      `[json-render/remotion] Rendering error in clip \"${this.props.clipId}\" (<${this.props.component}>):`,\n      error,\n      info.componentStack,\n    );\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return null;\n    }\n    return this.props.children;\n  }\n}\n\ninterface RendererProps {\n  /** The timeline spec to render */\n  spec: TimelineSpec;\n  /**\n   * Custom component registry to merge with standard components.\n   * Custom components override standard ones with the same name.\n   */\n  components?: ComponentRegistry;\n}\n\n/**\n * Renders a timeline spec into Remotion components\n *\n * @example\n * // Use with standard components only\n * <Renderer spec={mySpec} />\n *\n * @example\n * // Add custom components\n * <Renderer\n *   spec={mySpec}\n *   components={{\n *     CustomScene: MyCustomSceneComponent,\n *   }}\n * />\n */\nexport function Renderer({\n  spec,\n  components: customComponents,\n}: RendererProps) {\n  // Merge standard + custom components (custom overrides standard)\n  const components: ComponentRegistry = {\n    ...standardComponents,\n    ...customComponents,\n  };\n\n  if (!spec.clips || spec.clips.length === 0) {\n    return (\n      <AbsoluteFill\n        style={{\n          backgroundColor: \"#1a1a2e\",\n          color: \"#ffffff\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div style={{ fontSize: 24, opacity: 0.5 }}>No clips</div>\n      </AbsoluteFill>\n    );\n  }\n\n  // Separate main track clips from overlay clips\n  const mainClips = spec.clips.filter((c) => c.trackId === \"main\");\n  const overlayClips = spec.clips.filter((c) => c.trackId === \"overlay\");\n\n  const renderClip = (clip: Clip) => {\n    const Component = components[clip.component];\n    if (!Component) {\n      console.warn(`Unknown component: ${clip.component}`);\n      return null;\n    }\n\n    return (\n      <Sequence\n        key={clip.id}\n        from={clip.from}\n        durationInFrames={clip.durationInFrames}\n      >\n        <ClipErrorBoundary clipId={clip.id} component={clip.component}>\n          <Component clip={clip} />\n        </ClipErrorBoundary>\n      </Sequence>\n    );\n  };\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: \"#000000\" }}>\n      {/* Main track clips */}\n      {mainClips.map(renderClip)}\n\n      {/* Overlay clips */}\n      {overlayClips.map(renderClip)}\n    </AbsoluteFill>\n  );\n}\n"
  },
  {
    "path": "packages/remotion/src/components/hooks.ts",
    "content": "\"use client\";\n\nimport { useVideoConfig, spring, interpolate } from \"remotion\";\nimport type { Clip, TransitionStyles, MotionStyles } from \"./types\";\n\n/**\n * Calculate transition styles based on clip configuration\n *\n * Handles both transitionIn and transitionOut with support for:\n * - fade\n * - slideLeft / slideRight\n * - slideUp / slideDown\n * - zoom\n * - wipe\n */\nexport function useTransition(clip: Clip, frame: number): TransitionStyles {\n  const { fps } = useVideoConfig();\n  const relativeFrame = frame - clip.from;\n  const clipEnd = clip.durationInFrames;\n\n  let opacity = 1;\n  let translateX = 0;\n  let translateY = 0;\n  let scale = 1;\n\n  // Transition in\n  if (clip.transitionIn && relativeFrame < clip.transitionIn.durationInFrames) {\n    const progress = relativeFrame / clip.transitionIn.durationInFrames;\n    const easedProgress = spring({\n      frame: relativeFrame,\n      fps,\n      config: { damping: 200 },\n      durationInFrames: clip.transitionIn.durationInFrames,\n    });\n\n    switch (clip.transitionIn.type) {\n      case \"fade\":\n        opacity = easedProgress;\n        break;\n      case \"slideLeft\":\n        translateX = interpolate(easedProgress, [0, 1], [100, 0]);\n        opacity = easedProgress;\n        break;\n      case \"slideRight\":\n        translateX = interpolate(easedProgress, [0, 1], [-100, 0]);\n        opacity = easedProgress;\n        break;\n      case \"slideUp\":\n        translateY = interpolate(easedProgress, [0, 1], [100, 0]);\n        opacity = easedProgress;\n        break;\n      case \"slideDown\":\n        translateY = interpolate(easedProgress, [0, 1], [-100, 0]);\n        opacity = easedProgress;\n        break;\n      case \"zoom\":\n        scale = interpolate(easedProgress, [0, 1], [0.8, 1]);\n        opacity = easedProgress;\n        break;\n      case \"wipe\":\n        opacity = progress;\n        break;\n    }\n  }\n\n  // Transition out\n  if (\n    clip.transitionOut &&\n    relativeFrame > clipEnd - clip.transitionOut.durationInFrames\n  ) {\n    const outStart = clipEnd - clip.transitionOut.durationInFrames;\n    const outProgress =\n      (relativeFrame - outStart) / clip.transitionOut.durationInFrames;\n    const easedOutProgress = 1 - outProgress;\n\n    switch (clip.transitionOut.type) {\n      case \"fade\":\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n      case \"slideLeft\":\n        translateX = interpolate(outProgress, [0, 1], [0, -100]);\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n      case \"slideRight\":\n        translateX = interpolate(outProgress, [0, 1], [0, 100]);\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n      case \"slideUp\":\n        translateY = interpolate(outProgress, [0, 1], [0, -100]);\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n      case \"slideDown\":\n        translateY = interpolate(outProgress, [0, 1], [0, 100]);\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n      case \"zoom\":\n        scale = interpolate(outProgress, [0, 1], [1, 1.2]);\n        opacity = Math.min(opacity, easedOutProgress);\n        break;\n    }\n  }\n\n  return { opacity, translateX, translateY, scale };\n}\n\n/**\n * Calculate motion styles based on clip's motion configuration\n *\n * Handles declarative motion:\n * - enter: animate FROM values TO neutral\n * - exit: animate FROM neutral TO values\n * - loop: continuous animation during clip lifetime\n * - spring: physics-based easing\n */\nexport function useMotion(clip: Clip, frame: number): MotionStyles {\n  const { fps } = useVideoConfig();\n  const relativeFrame = frame - clip.from;\n  const clipEnd = clip.durationInFrames;\n\n  // Default neutral values\n  let opacity = 1;\n  let translateX = 0;\n  let translateY = 0;\n  let scale = 1;\n  let rotate = 0;\n\n  const motion = clip.motion;\n  if (!motion) {\n    return { opacity, translateX, translateY, scale, rotate };\n  }\n\n  // Spring config with defaults\n  const springConfig = {\n    damping: motion.spring?.damping ?? 20,\n    stiffness: motion.spring?.stiffness ?? 100,\n    mass: motion.spring?.mass ?? 1,\n  };\n\n  // Enter animation\n  if (motion.enter) {\n    const enterDuration = motion.enter.duration ?? 20;\n\n    if (relativeFrame < enterDuration) {\n      const progress = spring({\n        frame: relativeFrame,\n        fps,\n        config: springConfig,\n        durationInFrames: enterDuration,\n      });\n\n      // Interpolate FROM enter values TO neutral (1, 0, 0, 1, 0)\n      if (motion.enter.opacity !== undefined) {\n        opacity = interpolate(progress, [0, 1], [motion.enter.opacity, 1]);\n      }\n      if (motion.enter.scale !== undefined) {\n        scale = interpolate(progress, [0, 1], [motion.enter.scale, 1]);\n      }\n      if (motion.enter.x !== undefined) {\n        translateX = interpolate(progress, [0, 1], [motion.enter.x, 0]);\n      }\n      if (motion.enter.y !== undefined) {\n        translateY = interpolate(progress, [0, 1], [motion.enter.y, 0]);\n      }\n      if (motion.enter.rotate !== undefined) {\n        rotate = interpolate(progress, [0, 1], [motion.enter.rotate, 0]);\n      }\n    }\n  }\n\n  // Exit animation\n  if (motion.exit) {\n    const exitDuration = motion.exit.duration ?? 20;\n    const exitStart = clipEnd - exitDuration;\n\n    if (relativeFrame >= exitStart) {\n      const exitFrame = relativeFrame - exitStart;\n      const progress = spring({\n        frame: exitFrame,\n        fps,\n        config: springConfig,\n        durationInFrames: exitDuration,\n      });\n\n      // Interpolate FROM neutral TO exit values\n      if (motion.exit.opacity !== undefined) {\n        const exitOpacity = interpolate(\n          progress,\n          [0, 1],\n          [1, motion.exit.opacity],\n        );\n        opacity = Math.min(opacity, exitOpacity);\n      }\n      if (motion.exit.scale !== undefined) {\n        const exitScale = interpolate(progress, [0, 1], [1, motion.exit.scale]);\n        // Multiply scales for composition\n        scale = scale * exitScale;\n      }\n      if (motion.exit.x !== undefined) {\n        const exitX = interpolate(progress, [0, 1], [0, motion.exit.x]);\n        translateX = translateX + exitX;\n      }\n      if (motion.exit.y !== undefined) {\n        const exitY = interpolate(progress, [0, 1], [0, motion.exit.y]);\n        translateY = translateY + exitY;\n      }\n      if (motion.exit.rotate !== undefined) {\n        const exitRotate = interpolate(\n          progress,\n          [0, 1],\n          [0, motion.exit.rotate],\n        );\n        rotate = rotate + exitRotate;\n      }\n    }\n  }\n\n  // Loop animation (continuous during clip)\n  if (motion.loop) {\n    const { property, from, to, duration, easing = \"ease\" } = motion.loop;\n\n    // Calculate loop progress (0-1 repeating)\n    const loopFrame = relativeFrame % duration;\n    let loopProgress: number;\n\n    switch (easing) {\n      case \"linear\":\n        loopProgress = loopFrame / duration;\n        break;\n      case \"spring\":\n        loopProgress = spring({\n          frame: loopFrame,\n          fps,\n          config: springConfig,\n          durationInFrames: duration,\n        });\n        break;\n      case \"ease\":\n      default:\n        // Sine ease in-out for smooth looping\n        loopProgress = interpolate(\n          loopFrame,\n          [0, duration / 2, duration],\n          [0, 1, 0],\n          { extrapolateRight: \"clamp\" },\n        );\n        break;\n    }\n\n    const loopValue = interpolate(loopProgress, [0, 1], [from, to]);\n\n    switch (property) {\n      case \"opacity\":\n        opacity = opacity * loopValue;\n        break;\n      case \"scale\":\n        scale = scale * loopValue;\n        break;\n      case \"x\":\n        translateX = translateX + loopValue;\n        break;\n      case \"y\":\n        translateY = translateY + loopValue;\n        break;\n      case \"rotate\":\n        rotate = rotate + loopValue;\n        break;\n    }\n  }\n\n  return { opacity, translateX, translateY, scale, rotate };\n}\n"
  },
  {
    "path": "packages/remotion/src/components/index.ts",
    "content": "// Types\nexport type {\n  Clip,\n  TimelineSpec,\n  AudioTrack,\n  TransitionStyles,\n  MotionStyles,\n  Motion,\n  MotionState,\n  MotionLoop,\n  SpringConfig,\n  ClipComponent,\n  ComponentRegistry,\n} from \"./types\";\n\n// Hooks\nexport { useTransition, useMotion } from \"./hooks\";\n\n// Wrapper\nexport { ClipWrapper } from \"./ClipWrapper\";\n\n// Standard components\nexport {\n  TitleCard,\n  ImageSlide,\n  SplitScreen,\n  QuoteCard,\n  StatCard,\n  LowerThird,\n  TextOverlay,\n  TypingText,\n  LogoBug,\n  VideoClip,\n} from \"./standard\";\n\n// Renderer\nexport { Renderer, standardComponents } from \"./Renderer\";\n"
  },
  {
    "path": "packages/remotion/src/components/standard.tsx",
    "content": "\"use client\";\n\nimport {\n  AbsoluteFill,\n  useCurrentFrame,\n  useVideoConfig,\n  spring,\n} from \"remotion\";\nimport { ClipWrapper } from \"./ClipWrapper\";\nimport type { Clip } from \"./types\";\n\n// =============================================================================\n// TitleCard - Full-screen title with optional subtitle\n// =============================================================================\n\nexport function TitleCard({ clip }: { clip: Clip }) {\n  const { title, subtitle, backgroundColor, textColor } = clip.props as {\n    title: string;\n    subtitle?: string;\n    backgroundColor?: string;\n    textColor?: string;\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: backgroundColor || \"#1a1a2e\",\n          color: textColor || \"#ffffff\",\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          padding: 40,\n        }}\n      >\n        <div\n          style={{\n            fontSize: 72,\n            fontWeight: \"bold\",\n            textAlign: \"center\",\n            marginBottom: 16,\n          }}\n        >\n          {title}\n        </div>\n        {subtitle && (\n          <div\n            style={{\n              fontSize: 36,\n              opacity: 0.7,\n              textAlign: \"center\",\n            }}\n          >\n            {subtitle}\n          </div>\n        )}\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// ImageSlide - Full-screen image display\n// =============================================================================\n\nexport function ImageSlide({ clip }: { clip: Clip }) {\n  const { src, alt, fit, backgroundColor } = clip.props as {\n    src: string;\n    alt: string;\n    fit?: \"cover\" | \"contain\";\n    backgroundColor?: string;\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: backgroundColor || \"#0a0a0a\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        {src ? (\n          <img\n            src={src}\n            alt={alt}\n            style={{\n              width: \"100%\",\n              height: \"100%\",\n              objectFit: fit || \"cover\",\n            }}\n          />\n        ) : (\n          <div style={{ color: \"rgba(255,255,255,0.5)\", fontSize: 24 }}>\n            [{alt}]\n          </div>\n        )}\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// SplitScreen - Two-panel comparison view\n// =============================================================================\n\nexport function SplitScreen({ clip }: { clip: Clip }) {\n  const { leftTitle, rightTitle, leftColor, rightColor } = clip.props as {\n    leftTitle: string;\n    rightTitle: string;\n    leftColor?: string;\n    rightColor?: string;\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill style={{ display: \"flex\", flexDirection: \"row\" }}>\n        <div\n          style={{\n            flex: 1,\n            backgroundColor: leftColor || \"#1a1a2e\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#ffffff\",\n          }}\n        >\n          <div style={{ fontSize: 48, fontWeight: \"bold\" }}>{leftTitle}</div>\n        </div>\n        <div\n          style={{\n            flex: 1,\n            backgroundColor: rightColor || \"#2e1a1a\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#ffffff\",\n          }}\n        >\n          <div style={{ fontSize: 48, fontWeight: \"bold\" }}>{rightTitle}</div>\n        </div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// QuoteCard - Quote with attribution\n// =============================================================================\n\nexport function QuoteCard({ clip }: { clip: Clip }) {\n  const { quote, author, backgroundColor, textColor, transparent } =\n    clip.props as {\n      quote: string;\n      author?: string;\n      backgroundColor?: string;\n      textColor?: string;\n      transparent?: boolean;\n    };\n\n  // Use transparent background for overlays, or specified color, or default dark\n  const bgColor = transparent ? \"transparent\" : backgroundColor || \"#1a1a2e\";\n\n  const color = textColor || \"#ffffff\";\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: bgColor,\n          color,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          padding: 80,\n        }}\n      >\n        <div\n          style={{\n            fontSize: 48,\n            fontStyle: \"italic\",\n            textAlign: \"center\",\n            marginBottom: 24,\n            textShadow: transparent ? \"2px 2px 8px rgba(0,0,0,0.8)\" : \"none\",\n          }}\n        >\n          &ldquo;{quote}&rdquo;\n        </div>\n        {author && (\n          <div\n            style={{\n              fontSize: 28,\n              opacity: 0.9,\n              textShadow: transparent ? \"1px 1px 4px rgba(0,0,0,0.8)\" : \"none\",\n            }}\n          >\n            - {author}\n          </div>\n        )}\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// StatCard - Large statistic display with animation\n// =============================================================================\n\nexport function StatCard({ clip }: { clip: Clip }) {\n  const { value, label, prefix, suffix, backgroundColor } = clip.props as {\n    value: string | number;\n    label: string;\n    prefix?: string;\n    suffix?: string;\n    backgroundColor?: string;\n  };\n\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Animate the number counting up\n  const animationProgress = spring({\n    frame,\n    fps,\n    config: { damping: 100 },\n    durationInFrames: 30,\n  });\n\n  const numValue = typeof value === \"number\" ? value : parseFloat(value) || 0;\n  const displayValue = Math.round(numValue * animationProgress);\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: backgroundColor || \"#1a1a2e\",\n          color: \"#ffffff\",\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div style={{ fontSize: 96, fontWeight: \"bold\", marginBottom: 16 }}>\n          {prefix || \"\"}\n          {typeof value === \"number\" ? displayValue : value}\n          {suffix || \"\"}\n        </div>\n        <div style={{ fontSize: 32, opacity: 0.7 }}>{label}</div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// LowerThird - Name/title overlay\n// =============================================================================\n\nexport function LowerThird({ clip }: { clip: Clip }) {\n  const { name, title } = clip.props as {\n    name: string;\n    title?: string;\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill>\n        <div\n          style={{\n            position: \"absolute\",\n            bottom: 100,\n            left: 40,\n            backgroundColor: \"rgba(0,0,0,0.8)\",\n            color: \"#ffffff\",\n            padding: \"16px 24px\",\n            borderRadius: 8,\n          }}\n        >\n          <div style={{ fontSize: 28, fontWeight: \"bold\" }}>{name}</div>\n          {title && <div style={{ fontSize: 20, opacity: 0.7 }}>{title}</div>}\n        </div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// TextOverlay - Simple text overlay\n// =============================================================================\n\nexport function TextOverlay({ clip }: { clip: Clip }) {\n  const { text, position, fontSize } = clip.props as {\n    text: string;\n    position?: \"top\" | \"center\" | \"bottom\";\n    fontSize?: \"small\" | \"medium\" | \"large\";\n  };\n\n  const positionStyles: Record<string, React.CSSProperties> = {\n    top: { top: 100, left: 0, right: 0 },\n    center: { top: \"50%\", left: 0, right: 0, transform: \"translateY(-50%)\" },\n    bottom: { bottom: 100, left: 0, right: 0 },\n  };\n\n  const fontSizes: Record<string, number> = {\n    small: 24,\n    medium: 36,\n    large: 56,\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill>\n        <div\n          style={{\n            position: \"absolute\",\n            ...positionStyles[position || \"center\"],\n            textAlign: \"center\",\n            color: \"#ffffff\",\n            fontSize: fontSizes[fontSize || \"medium\"],\n            padding: 20,\n            textShadow: \"2px 2px 4px rgba(0,0,0,0.5)\",\n          }}\n        >\n          {text}\n        </div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// TypingText - Terminal-style typing animation\n// =============================================================================\n\nexport function TypingText({ clip }: { clip: Clip }) {\n  const {\n    text,\n    backgroundColor,\n    textColor,\n    fontSize,\n    fontFamily,\n    showCursor = true,\n    cursorChar = \"|\",\n    charsPerSecond = 15,\n  } = clip.props as {\n    text: string;\n    backgroundColor?: string;\n    textColor?: string;\n    fontSize?: number;\n    fontFamily?: \"monospace\" | \"sans-serif\" | \"serif\";\n    showCursor?: boolean;\n    cursorChar?: string;\n    charsPerSecond?: number;\n  };\n\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // Calculate how many characters to show based on current frame\n  const framesPerChar = fps / charsPerSecond;\n  const charsToShow = Math.min(Math.floor(frame / framesPerChar), text.length);\n  const displayedText = text.slice(0, charsToShow);\n  const isTypingComplete = charsToShow >= text.length;\n\n  // Blinking cursor (blinks every 0.5 seconds)\n  const cursorVisible =\n    showCursor &&\n    (Math.floor(frame / (fps / 2)) % 2 === 0 || !isTypingComplete);\n\n  const fontFamilyMap: Record<string, string> = {\n    monospace: \"'Courier New', Consolas, monospace\",\n    \"sans-serif\": \"system-ui, -apple-system, sans-serif\",\n    serif: \"Georgia, 'Times New Roman', serif\",\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: backgroundColor || \"#1e1e1e\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          padding: 60,\n        }}\n      >\n        <div\n          style={{\n            color: textColor || \"#00ff00\",\n            fontSize: fontSize || 48,\n            fontFamily: fontFamilyMap[fontFamily || \"monospace\"],\n            whiteSpace: \"pre-wrap\",\n            wordBreak: \"break-word\",\n            maxWidth: \"90%\",\n            textAlign: \"left\",\n          }}\n        >\n          {displayedText}\n          {cursorVisible && (\n            <span\n              style={{\n                opacity: isTypingComplete\n                  ? Math.floor(frame / (fps / 2)) % 2 === 0\n                    ? 1\n                    : 0\n                  : 1,\n              }}\n            >\n              {cursorChar}\n            </span>\n          )}\n        </div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// LogoBug - Corner watermark/logo\n// =============================================================================\n\nexport function LogoBug({ clip }: { clip: Clip }) {\n  const { position, opacity: propOpacity } = clip.props as {\n    position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n    opacity?: number;\n  };\n\n  const positionStyles: Record<string, React.CSSProperties> = {\n    \"top-left\": { top: 20, left: 20 },\n    \"top-right\": { top: 20, right: 20 },\n    \"bottom-left\": { bottom: 20, left: 20 },\n    \"bottom-right\": { bottom: 20, right: 20 },\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill>\n        <div\n          style={{\n            position: \"absolute\",\n            ...positionStyles[position || \"bottom-right\"],\n            opacity: propOpacity ?? 0.5,\n            color: \"#ffffff\",\n            fontSize: 14,\n            fontWeight: \"bold\",\n            textShadow: \"1px 1px 2px rgba(0,0,0,0.5)\",\n          }}\n        >\n          LOGO\n        </div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n\n// =============================================================================\n// VideoClip - Video file playback (placeholder)\n// =============================================================================\n\nexport function VideoClip({ clip }: { clip: Clip }) {\n  const { src } = clip.props as {\n    src: string;\n    startFrom?: number;\n    volume?: number;\n  };\n\n  return (\n    <ClipWrapper clip={clip}>\n      <AbsoluteFill\n        style={{\n          backgroundColor: \"#000000\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: \"rgba(255,255,255,0.5)\",\n        }}\n      >\n        <div>[Video: {src}]</div>\n      </AbsoluteFill>\n    </ClipWrapper>\n  );\n}\n"
  },
  {
    "path": "packages/remotion/src/components/types.ts",
    "content": "/**\n * Types for Remotion timeline components\n */\n\n/**\n * Motion state for enter/exit animations\n * Values animate FROM these values TO neutral (enter) or FROM neutral TO these values (exit)\n */\nexport interface MotionState {\n  /** Opacity (0-1), default: 1 */\n  opacity?: number;\n  /** Scale multiplier (0.8 = 80%), default: 1 */\n  scale?: number;\n  /** Translate X in pixels, default: 0 */\n  x?: number;\n  /** Translate Y in pixels, default: 0 */\n  y?: number;\n  /** Rotation in degrees, default: 0 */\n  rotate?: number;\n  /** Duration in frames, default: 20 */\n  duration?: number;\n}\n\n/**\n * Spring physics configuration\n */\nexport interface SpringConfig {\n  /** Damping coefficient, default: 20 */\n  damping?: number;\n  /** Stiffness, default: 100 */\n  stiffness?: number;\n  /** Mass, default: 1 */\n  mass?: number;\n}\n\n/**\n * Continuous loop animation\n */\nexport interface MotionLoop {\n  /** Property to animate */\n  property: \"scale\" | \"rotate\" | \"x\" | \"y\" | \"opacity\";\n  /** Starting value */\n  from: number;\n  /** Ending value */\n  to: number;\n  /** Duration of one cycle in frames */\n  duration: number;\n  /** Easing type, default: \"ease\" */\n  easing?: \"linear\" | \"ease\" | \"spring\";\n}\n\n/**\n * Declarative motion configuration for clips\n */\nexport interface Motion {\n  /** Enter animation - animates FROM these values TO neutral */\n  enter?: MotionState;\n  /** Exit animation - animates FROM neutral TO these values */\n  exit?: MotionState;\n  /** Spring physics config (applies to enter/exit) */\n  spring?: SpringConfig;\n  /** Continuous looping animation */\n  loop?: MotionLoop;\n}\n\n/**\n * Motion styles calculated by useMotion hook\n */\nexport interface MotionStyles {\n  opacity: number;\n  translateX: number;\n  translateY: number;\n  scale: number;\n  rotate: number;\n}\n\n/**\n * Clip data passed to components\n */\nexport interface Clip {\n  id: string;\n  trackId: string;\n  component: string;\n  props: Record<string, unknown>;\n  from: number;\n  durationInFrames: number;\n  transitionIn?: { type: string; durationInFrames: number };\n  transitionOut?: { type: string; durationInFrames: number };\n  /** Declarative motion configuration */\n  motion?: Motion;\n}\n\n/**\n * Timeline spec structure\n */\nexport interface TimelineSpec {\n  composition?: {\n    id: string;\n    fps: number;\n    width: number;\n    height: number;\n    durationInFrames: number;\n  };\n  tracks?: { id: string; name: string; type: string; enabled: boolean }[];\n  clips?: Clip[];\n  audio?: { tracks: AudioTrack[] };\n}\n\n/**\n * Audio track in the timeline\n */\nexport interface AudioTrack {\n  id: string;\n  src: string;\n  from: number;\n  durationInFrames: number;\n  volume: number;\n}\n\n/**\n * Transition styles calculated by useTransition hook\n */\nexport interface TransitionStyles {\n  opacity: number;\n  translateX: number;\n  translateY: number;\n  scale: number;\n}\n\n/**\n * Component render function type\n */\nexport type ClipComponent = React.ComponentType<{ clip: Clip }>;\n\n/**\n * Component registry type\n */\nexport type ComponentRegistry = Record<string, ClipComponent>;\n"
  },
  {
    "path": "packages/remotion/src/index.ts",
    "content": "// Schema (Remotion's timeline-based spec format)\nexport { schema, type RemotionSchema, type RemotionSpec } from \"./schema\";\n\n// Catalog-aware types for Remotion\nexport type {\n  FrameContext,\n  VideoComponentContext,\n  VideoComponentFn,\n  VideoComponents,\n  TransitionFn,\n  BuiltInTransition,\n  EffectFn,\n  Effects,\n} from \"./catalog-types\";\n\n// Core types (re-exported for convenience)\nexport type { Spec } from \"@json-render/core\";\n\n// =============================================================================\n// Components - Pre-built Remotion components for rendering timelines\n// =============================================================================\n\n// Component types\nexport type {\n  Clip,\n  TimelineSpec,\n  AudioTrack,\n  TransitionStyles,\n  MotionStyles,\n  Motion,\n  MotionState,\n  MotionLoop,\n  SpringConfig,\n  ClipComponent,\n  ComponentRegistry,\n} from \"./components\";\n\n// Hooks and utilities\nexport { useTransition, useMotion, ClipWrapper } from \"./components\";\n\n// Standard components\nexport {\n  TitleCard,\n  ImageSlide,\n  SplitScreen,\n  QuoteCard,\n  StatCard,\n  LowerThird,\n  TextOverlay,\n  TypingText,\n  LogoBug,\n  VideoClip,\n} from \"./components\";\n\n// Renderer and component registry\nexport { Renderer, standardComponents } from \"./components\";\n\n// =============================================================================\n// Catalog Definitions - Pre-built definitions for use in catalogs\n// =============================================================================\n\nexport {\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n  type ComponentDefinition,\n  type TransitionDefinition,\n  type EffectDefinition,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/remotion/src/schema.ts",
    "content": "import { defineSchema, type PromptContext } from \"@json-render/core\";\n\n/**\n * Prompt template for Remotion timeline generation\n *\n * Uses JSONL patch format (same as React) but builds up a timeline spec structure.\n */\nfunction remotionPromptTemplate(context: PromptContext): string {\n  const { catalog, options } = context;\n  const { system = \"You are a video timeline generator.\", customRules = [] } =\n    options;\n\n  const lines: string[] = [];\n  lines.push(system);\n  lines.push(\"\");\n\n  // Output format - JSONL patches\n  lines.push(\"OUTPUT FORMAT:\");\n  lines.push(\n    \"Output JSONL (one JSON object per line) with patches to build a timeline spec.\",\n  );\n  lines.push(\n    \"Each line is a JSON patch operation. Build the timeline incrementally.\",\n  );\n  lines.push(\"\");\n  lines.push(\"Example output (each line is a separate JSON object):\");\n  lines.push(\"\");\n  lines.push(`{\"op\":\"add\",\"path\":\"/composition\",\"value\":{\"id\":\"intro\",\"fps\":30,\"width\":1920,\"height\":1080,\"durationInFrames\":300}}\n{\"op\":\"add\",\"path\":\"/tracks\",\"value\":[{\"id\":\"main\",\"name\":\"Main\",\"type\":\"video\",\"enabled\":true},{\"id\":\"overlay\",\"name\":\"Overlay\",\"type\":\"overlay\",\"enabled\":true}]}\n{\"op\":\"add\",\"path\":\"/clips\",\"value\":[]}\n{\"op\":\"add\",\"path\":\"/clips/-\",\"value\":{\"id\":\"clip-1\",\"trackId\":\"main\",\"component\":\"TitleCard\",\"props\":{\"title\":\"Welcome\",\"subtitle\":\"Getting Started\"},\"from\":0,\"durationInFrames\":90,\"transitionIn\":{\"type\":\"fade\",\"durationInFrames\":15},\"transitionOut\":{\"type\":\"fade\",\"durationInFrames\":15},\"motion\":{\"enter\":{\"opacity\":0,\"y\":50,\"scale\":0.9,\"duration\":25},\"spring\":{\"damping\":15}}}}\n{\"op\":\"add\",\"path\":\"/clips/-\",\"value\":{\"id\":\"clip-2\",\"trackId\":\"main\",\"component\":\"TitleCard\",\"props\":{\"title\":\"Features\"},\"from\":90,\"durationInFrames\":90,\"motion\":{\"enter\":{\"opacity\":0,\"x\":-100,\"duration\":20},\"exit\":{\"opacity\":0,\"x\":100,\"duration\":15}}}}\n{\"op\":\"add\",\"path\":\"/audio\",\"value\":{\"tracks\":[]}}`);\n  lines.push(\"\");\n\n  // Components\n  const catalogData = catalog as {\n    components?: Record<\n      string,\n      { description?: string; defaultDuration?: number }\n    >;\n    transitions?: Record<string, { description?: string }>;\n    effects?: Record<string, { description?: string }>;\n  };\n\n  if (catalogData.components) {\n    lines.push(\n      `AVAILABLE COMPONENTS (${Object.keys(catalogData.components).length}):`,\n    );\n    lines.push(\"\");\n    for (const [name, def] of Object.entries(catalogData.components)) {\n      const duration = def.defaultDuration\n        ? ` [default: ${def.defaultDuration} frames]`\n        : \"\";\n      lines.push(\n        `- ${name}: ${def.description || \"No description\"}${duration}`,\n      );\n    }\n    lines.push(\"\");\n  }\n\n  // Transitions\n  if (\n    catalogData.transitions &&\n    Object.keys(catalogData.transitions).length > 0\n  ) {\n    lines.push(\"AVAILABLE TRANSITIONS:\");\n    lines.push(\"\");\n    for (const [name, def] of Object.entries(catalogData.transitions)) {\n      lines.push(`- ${name}: ${def.description || \"No description\"}`);\n    }\n    lines.push(\"\");\n  }\n\n  // Motion system documentation\n  lines.push(\"MOTION SYSTEM:\");\n  lines.push(\n    \"Clips can have a 'motion' field for declarative animations (optional, use for dynamic/engaging videos):\",\n  );\n  lines.push(\"\");\n  lines.push(\n    \"- enter: {opacity?, scale?, x?, y?, rotate?, duration?} - animate FROM these values TO normal when clip starts\",\n  );\n  lines.push(\n    \"- exit: {opacity?, scale?, x?, y?, rotate?, duration?} - animate FROM normal TO these values when clip ends\",\n  );\n  lines.push(\n    \"- spring: {damping?, stiffness?, mass?} - physics config (lower damping = more bounce)\",\n  );\n  lines.push(\n    '- loop: {property, from, to, duration, easing?} - continuous animation (property: \"scale\"|\"rotate\"|\"x\"|\"y\"|\"opacity\")',\n  );\n  lines.push(\"\");\n  lines.push(\"Example motion configs:\");\n  lines.push('  Fade up: {\"enter\":{\"opacity\":0,\"y\":30,\"duration\":20}}');\n  lines.push(\n    '  Scale pop: {\"enter\":{\"scale\":0.5,\"opacity\":0,\"duration\":15},\"spring\":{\"damping\":10}}',\n  );\n  lines.push(\n    '  Slide in/out: {\"enter\":{\"x\":-100,\"duration\":20},\"exit\":{\"x\":100,\"duration\":15}}',\n  );\n  lines.push(\n    '  Gentle pulse: {\"loop\":{\"property\":\"scale\",\"from\":1,\"to\":1.05,\"duration\":60,\"easing\":\"ease\"}}',\n  );\n  lines.push(\"\");\n\n  // Rules\n  lines.push(\"RULES:\");\n  const baseRules = [\n    \"Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences\",\n    'First add /composition with {id, fps:30, width:1920, height:1080, durationInFrames}: {\"op\":\"add\",\"path\":\"/composition\",\"value\":{...}}',\n    'Then add /tracks array with video/overlay tracks: {\"op\":\"add\",\"path\":\"/tracks\",\"value\":[...]}',\n    'Then add each clip by appending to the array: {\"op\":\"add\",\"path\":\"/clips/-\",\"value\":{...}}',\n    'Finally add /audio with {tracks:[]}: {\"op\":\"add\",\"path\":\"/audio\",\"value\":{...}}',\n    \"ONLY use components listed above\",\n    \"fps is always 30 (1 second = 30 frames, 10 seconds = 300 frames)\",\n    'Clips on \"main\" track flow sequentially (from = previous clip\\'s from + durationInFrames)',\n    'Overlay clips (LowerThird, TextOverlay) go on \"overlay\" track',\n    \"Use motion.enter for engaging clip entrances, motion.exit for smooth departures\",\n    \"Spring damping: 20=smooth, 10=bouncy, 5=very bouncy\",\n  ];\n  const allRules = [...baseRules, ...customRules];\n  allRules.forEach((rule, i) => {\n    lines.push(`${i + 1}. ${rule}`);\n  });\n\n  return lines.join(\"\\n\");\n}\n\n/**\n * The schema for @json-render/remotion\n *\n * This schema is fundamentally different from the React element tree schema.\n * It's timeline-based, designed for video composition:\n *\n * - Spec: A composition with tracks containing timed clips\n * - Catalog: Video components (scenes, overlays, etc.) and effects\n *\n * This demonstrates that json-render is truly agnostic - different renderers\n * can have completely different spec formats.\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like (timeline-based)\n    spec: s.object({\n      /** Composition settings */\n      composition: s.object({\n        /** Unique composition ID */\n        id: s.string(),\n        /** Frames per second */\n        fps: s.number(),\n        /** Width in pixels */\n        width: s.number(),\n        /** Height in pixels */\n        height: s.number(),\n        /** Total duration in frames */\n        durationInFrames: s.number(),\n      }),\n\n      /** Timeline tracks (like layers in video editing) */\n      tracks: s.array(\n        s.object({\n          /** Unique track ID */\n          id: s.string(),\n          /** Track name for organization */\n          name: s.string(),\n          /** Track type: \"video\" | \"audio\" | \"overlay\" | \"text\" */\n          type: s.string(),\n          /** Whether track is muted/hidden */\n          enabled: s.boolean(),\n        }),\n      ),\n\n      /** Clips placed on the timeline */\n      clips: s.array(\n        s.object({\n          /** Unique clip ID */\n          id: s.string(),\n          /** Which track this clip belongs to */\n          trackId: s.string(),\n          /** Component type from catalog */\n          component: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Start frame (when clip begins) */\n          from: s.number(),\n          /** Duration in frames */\n          durationInFrames: s.number(),\n          /** Transition in effect */\n          transitionIn: s.object({\n            type: s.ref(\"catalog.transitions\"),\n            durationInFrames: s.number(),\n          }),\n          /** Transition out effect */\n          transitionOut: s.object({\n            type: s.ref(\"catalog.transitions\"),\n            durationInFrames: s.number(),\n          }),\n          /** Declarative motion configuration for custom animations */\n          motion: s.object({\n            /** Enter animation - animates FROM these values TO neutral */\n            enter: s.object({\n              /** Starting opacity (0-1), animates to 1 */\n              opacity: s.number(),\n              /** Starting scale (e.g., 0.8 = 80%), animates to 1 */\n              scale: s.number(),\n              /** Starting X offset in pixels, animates to 0 */\n              x: s.number(),\n              /** Starting Y offset in pixels, animates to 0 */\n              y: s.number(),\n              /** Starting rotation in degrees, animates to 0 */\n              rotate: s.number(),\n              /** Duration of enter animation in frames (default: 20) */\n              duration: s.number(),\n            }),\n            /** Exit animation - animates FROM neutral TO these values */\n            exit: s.object({\n              /** Ending opacity (0-1), animates from 1 */\n              opacity: s.number(),\n              /** Ending scale, animates from 1 */\n              scale: s.number(),\n              /** Ending X offset in pixels, animates from 0 */\n              x: s.number(),\n              /** Ending Y offset in pixels, animates from 0 */\n              y: s.number(),\n              /** Ending rotation in degrees, animates from 0 */\n              rotate: s.number(),\n              /** Duration of exit animation in frames (default: 20) */\n              duration: s.number(),\n            }),\n            /** Spring physics configuration */\n            spring: s.object({\n              /** Damping coefficient (default: 20) */\n              damping: s.number(),\n              /** Stiffness (default: 100) */\n              stiffness: s.number(),\n              /** Mass (default: 1) */\n              mass: s.number(),\n            }),\n            /** Continuous looping animation */\n            loop: s.object({\n              /** Property to animate: \"scale\" | \"rotate\" | \"x\" | \"y\" | \"opacity\" */\n              property: s.string(),\n              /** Starting value */\n              from: s.number(),\n              /** Ending value */\n              to: s.number(),\n              /** Duration of one cycle in frames */\n              duration: s.number(),\n              /** Easing type: \"linear\" | \"ease\" | \"spring\" (default: \"ease\") */\n              easing: s.string(),\n            }),\n          }),\n        }),\n      ),\n\n      /** Audio configuration */\n      audio: s.object({\n        /** Background music/audio clips */\n        tracks: s.array(\n          s.object({\n            id: s.string(),\n            src: s.string(),\n            from: s.number(),\n            durationInFrames: s.number(),\n            volume: s.number(),\n          }),\n        ),\n      }),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Video component definitions (scenes, overlays, etc.) */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Component type: \"scene\" | \"overlay\" | \"text\" | \"image\" | \"video\" */\n        type: s.string(),\n        /** Default duration in frames (can be overridden per clip) */\n        defaultDuration: s.number(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n      /** Transition effect definitions */\n      transitions: s.map({\n        /** Default duration in frames */\n        defaultDuration: s.number(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n      /** Effect definitions (filters, animations, etc.) */\n      effects: s.map({\n        /** Zod schema for effect params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    promptTemplate: remotionPromptTemplate,\n  },\n);\n\n/**\n * Type for the Remotion schema\n */\nexport type RemotionSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type RemotionSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/remotion/src/server.ts",
    "content": "/**\n * Server-safe exports for @json-render/remotion\n *\n * This entry point only exports schema and catalog definitions,\n * without any React or Remotion runtime dependencies.\n * Use this in server components, API routes, and build scripts.\n *\n * @example\n * ```ts\n * // In an API route or server component\n * import { schema, standardComponentDefinitions } from \"@json-render/remotion/server\";\n * ```\n */\n\n// Schema (no React dependencies)\nexport { schema, type RemotionSchema, type RemotionSpec } from \"./schema\";\n\n// Catalog definitions (no React dependencies)\nexport {\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n  type ComponentDefinition,\n  type TransitionDefinition,\n  type EffectDefinition,\n} from \"./catalog\";\n\n// Catalog types (type-only exports)\nexport type {\n  FrameContext,\n  VideoComponentContext,\n  VideoComponentFn,\n  VideoComponents,\n  TransitionFn,\n  BuiltInTransition,\n  EffectFn,\n  Effects,\n} from \"./catalog-types\";\n\n// Core types (re-exported for convenience)\nexport type { Spec } from \"@json-render/core\";\n"
  },
  {
    "path": "packages/remotion/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/remotion/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/server.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"react\", \"react-dom\", \"remotion\", \"@json-render/core\", \"zod\"],\n});\n"
  },
  {
    "path": "packages/shadcn/CHANGELOG.md",
    "content": "# @json-render/shadcn\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n  - @json-render/react@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n  - @json-render/react@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n  - @json-render/react@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n  - @json-render/react@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n  - @json-render/react@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n  - @json-render/react@0.11.0\n\n## 0.10.0\n\n### Minor Changes\n\n- 9cef4e9: Dynamic forms, Vue renderer, XState Store adapter, and computed values.\n\n  ### New: `@json-render/vue` Package\n\n  Vue 3 renderer for json-render. Full feature parity with `@json-render/react` including data binding, visibility conditions, actions, validation, repeat scopes, and streaming.\n  - `defineRegistry` — create type-safe component registries from catalogs\n  - `Renderer` — render specs as Vue component trees\n  - Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`\n  - Composables: `useStateStore`, `useStateValue`, `useStateBinding`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`\n  - Streaming: `useUIStream`, `useChatUI`\n  - External store support via `StateStore` interface\n\n  ### New: `@json-render/xstate` Package\n\n  XState Store (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend.\n  - `xstateStoreStateStore({ atom })` — creates a `StateStore` from an `@xstate/store` atom\n  - Requires `@xstate/store` v3+\n\n  ### New: `$computed` Expressions\n\n  Call registered functions from prop expressions:\n  - `{ \"$computed\": \"functionName\", \"args\": { \"key\": <expression> } }` — calls a named function with resolved args\n  - Functions registered via catalog and provided at runtime through `functions` prop on `JSONUIProvider` / `createRenderer`\n  - `ComputedFunction` type exported from `@json-render/core`\n\n  ### New: `$template` Expressions\n\n  Interpolate state values into strings:\n  - `{ \"$template\": \"Hello, ${/user/name}!\" }` — replaces `${/path}` references with state values\n  - Missing paths resolve to empty string\n\n  ### New: State Watchers\n\n  React to state changes by triggering actions:\n  - `watch` field on elements maps state paths to action bindings\n  - Fires when watched values change (not on initial render)\n  - Supports cascading dependencies (e.g. country → city loading)\n  - `watch` is a top-level field on elements (sibling of type/props/children), not inside props\n  - Spec validator detects and auto-fixes `watch` placed inside props\n\n  ### New: Cross-Field Validation Functions\n\n  New built-in validation functions for cross-field comparisons:\n  - `equalTo` — alias for `matches` with clearer semantics\n  - `lessThan` — value must be less than another field (numbers, strings, coerced)\n  - `greaterThan` — value must be greater than another field\n  - `requiredIf` — required only when a condition field is truthy\n  - Validation args now resolve through `resolvePropValue` for consistent `$state` expression handling\n\n  ### New: `validateForm` Action (React)\n\n  Built-in action that validates all registered form fields at once:\n  - Runs `validateAll()` synchronously and writes `{ valid, errors }` to state\n  - Default state path: `/formValidation` (configurable via `statePath` param)\n  - Added to React schema's built-in actions list\n\n  ### Improved: shadcn/ui Validation\n\n  All form components now support validation:\n  - Checkbox, Radio, Switch — added `checks` and `validateOn` props\n  - Input, Textarea, Select — added `validateOn` prop (controls timing: change/blur/submit)\n  - Shared validation schemas reduce catalog definition duplication\n\n  ### Improved: React Provider Tree\n\n  Reordered provider nesting so `ValidationProvider` wraps `ActionProvider`, enabling `validateForm` to access validation state. Added `useOptionalValidation` hook for non-throwing access.\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n  - @json-render/react@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- Updated dependencies [b103676]\n  - @json-render/react@0.9.1\n  - @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n  - @json-render/react@0.9.0\n\n## 0.8.0\n\n### Patch Changes\n\n- Updated dependencies [09376db]\n  - @json-render/core@0.8.0\n  - @json-render/react@0.8.0\n\n## 0.7.0\n\n### Minor Changes\n\n- 2d70fab: New `@json-render/shadcn` package, event handles, built-in actions, and stream improvements.\n\n  ### New: `@json-render/shadcn` Package\n\n  Pre-built [shadcn/ui](https://ui.shadcn.com/) component library for json-render. 30+ components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.\n  - `shadcnComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/shadcn/catalog`)\n  - `shadcnComponents` — React implementations for all components\n  - Layout: Card, Stack, Grid, Separator\n  - Navigation: Tabs, Accordion, Collapsible, Pagination\n  - Overlay: Dialog, Drawer, Tooltip, Popover, DropdownMenu\n  - Content: Heading, Text, Image, Avatar, Badge, Alert, Carousel, Table\n  - Feedback: Progress, Skeleton, Spinner\n  - Input: Button, Link, Input, Textarea, Select, Checkbox, Radio, Switch, Slider, Toggle, ToggleGroup, ButtonGroup\n\n  ### New: Event Handles (`on()`)\n\n  Components now receive an `on(event)` function in addition to `emit(event)`. The `on()` function returns an `EventHandle` with metadata:\n  - `emit()` — fire the event\n  - `shouldPreventDefault` — whether any action binding requested `preventDefault`\n  - `bound` — whether any handler is bound to this event\n\n  ### New: `BaseComponentProps`\n\n  Catalog-agnostic base type for component render functions. Use when building reusable component libraries (like `@json-render/shadcn`) that are not tied to a specific catalog.\n\n  ### New: Built-in Actions in Schema\n\n  Schemas can now declare `builtInActions` — actions that are always available at runtime and automatically injected into prompts. The React schema declares `setState`, `pushState`, and `removeState` as built-in, so they appear in prompts without needing to be listed in catalog `actions`.\n\n  ### New: `preventDefault` on `ActionBinding`\n\n  Action bindings now support a `preventDefault` boolean field, allowing the LLM to request that default browser behavior (e.g. navigation on links) be prevented.\n\n  ### Improved: Stream Transform Text Block Splitting\n\n  `createJsonRenderTransform()` now properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs. This ensures the AI SDK creates separate text parts, preserving correct interleaving of prose and UI in `message.parts`.\n\n  ### Improved: `defineRegistry` Actions Requirement\n\n  `defineRegistry` now conditionally requires the `actions` field only when the catalog declares actions. Catalogs with no actions (e.g. `actions: {}`) no longer need to pass an empty actions object.\n\n### Patch Changes\n\n- Updated dependencies [2d70fab]\n  - @json-render/core@0.7.0\n  - @json-render/react@0.7.0\n"
  },
  {
    "path": "packages/shadcn/README.md",
    "content": "# @json-render/shadcn\n\nPre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. Drop-in catalog definitions and React implementations for 36 components built on Radix UI + Tailwind CSS.\n\n## Installation\n\n```bash\nnpm install @json-render/shadcn @json-render/core @json-render/react zod\n```\n\n## Quick Start\n\n### 1. Create a Catalog\n\nImport standard definitions from `@json-render/shadcn/catalog` and pass them to `defineCatalog`:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    // Pick the components you need\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Heading: shadcnComponentDefinitions.Heading,\n    Button: shadcnComponentDefinitions.Button,\n    Input: shadcnComponentDefinitions.Input,\n  },\n  actions: {},\n});\n```\n\n> **Note:** State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled automatically by `ActionProvider`. You don't need to declare them in your catalog.\n\n### 2. Create a Registry\n\nImport standard implementations from `@json-render/shadcn` and pass them to `defineRegistry`:\n\n```typescript\nimport { defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Heading: shadcnComponents.Heading,\n    Button: shadcnComponents.Button,\n    Input: shadcnComponents.Input,\n  },\n});\n```\n\n### 3. Render\n\n```tsx\nimport { Renderer } from \"@json-render/react\";\n\nfunction App({ spec }) {\n  return <Renderer spec={spec} registry={registry} />;\n}\n```\n\n## Extending with Custom Components\n\nPick standard components as a base and add your own alongside them:\n\n```typescript\nimport { z } from \"zod\";\n\n// Catalog\nconst catalog = defineCatalog(schema, {\n  components: {\n    // Standard\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Button: shadcnComponentDefinitions.Button,\n\n    // Custom\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        trend: z.enum([\"up\", \"down\", \"neutral\"]).nullable(),\n      }),\n      description: \"KPI metric display\",\n    },\n  },\n  actions: {},\n});\n\n// Registry\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    // Standard\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Button: shadcnComponents.Button,\n\n    // Custom\n    Metric: ({ props }) => (\n      <div>\n        <span>{props.label}</span>\n        <span>{props.value}</span>\n      </div>\n    ),\n  },\n});\n```\n\n## Standard Components\n\n### Layout\n\n| Component | Description |\n|-----------|-------------|\n| `Card` | Container card with optional title and description |\n| `Stack` | Flex container (horizontal/vertical) with gap, alignment, justify |\n| `Grid` | Grid layout (1-6 columns) |\n| `Separator` | Visual separator line |\n\n### Navigation\n\n| Component | Description |\n|-----------|-------------|\n| `Tabs` | Tabbed navigation |\n| `Accordion` | Collapsible accordion sections |\n| `Collapsible` | Single collapsible section with trigger |\n| `Pagination` | Page navigation |\n\n### Overlay\n\n| Component | Description |\n|-----------|-------------|\n| `Dialog` | Modal dialog |\n| `Drawer` | Bottom drawer |\n| `Tooltip` | Hover tooltip |\n| `Popover` | Click-triggered popover |\n| `DropdownMenu` | Dropdown menu with selectable items |\n\n### Content\n\n| Component | Description |\n|-----------|-------------|\n| `Heading` | Heading text (h1-h4) |\n| `Text` | Paragraph text with variants (body, caption, muted, lead, code) |\n| `Image` | Placeholder image |\n| `Avatar` | User avatar with fallback initials |\n| `Badge` | Status badge |\n| `Alert` | Alert banner |\n| `Carousel` | Horizontally scrollable carousel |\n| `Table` | Data table with columns and rows |\n\n### Feedback\n\n| Component | Description |\n|-----------|-------------|\n| `Progress` | Progress bar |\n| `Skeleton` | Loading placeholder |\n| `Spinner` | Loading spinner |\n\n### Input\n\n| Component | Description |\n|-----------|-------------|\n| `Button` | Clickable button with variants |\n| `Link` | Anchor link |\n| `Input` | Text input with label, validation, and `validateOn` timing |\n| `Textarea` | Multi-line text input with validation and `validateOn` |\n| `Select` | Dropdown select with validation and `validateOn` |\n| `Checkbox` | Checkbox input with validation and `validateOn` |\n| `Radio` | Radio button group with validation and `validateOn` |\n| `Switch` | Toggle switch with validation and `validateOn` |\n| `Slider` | Range slider |\n| `Toggle` | Toggle button |\n| `ToggleGroup` | Group of toggle buttons |\n| `ButtonGroup` | Group of buttons with selected state |\n\n## Built-in Actions\n\nState actions (`setState`, `pushState`, `removeState`, `validateForm`) are built into the `@json-render/react` schema and handled automatically by `ActionProvider`. They are included in prompts without needing to be declared in your catalog.\n\n| Action | Description |\n|--------|-------------|\n| `setState` | Set a value at a state path |\n| `pushState` | Push a value onto an array in state |\n| `removeState` | Remove an item from an array in state |\n| `validateForm` | Validate all fields and write result to state |\n\n### Validation Timing (`validateOn`)\n\nAll form components support the `validateOn` prop to control when validation runs:\n\n| Value | Description | Default For |\n|-------|-------------|-------------|\n| `\"change\"` | Validate on every input change | Select, Checkbox, Radio, Switch |\n| `\"blur\"` | Validate when field loses focus | Input, Textarea |\n| `\"submit\"` | Validate only on form submission | — |\n\n## Exports\n\n| Entry Point | Exports |\n|-------------|---------|\n| `@json-render/shadcn` | `shadcnComponents` |\n| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` |\n\nThe `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation.\n"
  },
  {
    "path": "packages/shadcn/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"\",\n    \"css\": \"\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"src\",\n    \"utils\": \"src/lib/utils\",\n    \"ui\": \"src/ui\",\n    \"lib\": \"src/lib\",\n    \"hooks\": \"src/hooks\"\n  }\n}\n"
  },
  {
    "path": "packages/shadcn/package.json",
    "content": "{\n  \"name\": \"@json-render/shadcn\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"react\",\n    \"shadcn\",\n    \"tailwind\",\n    \"radix\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/shadcn\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./catalog\": {\n      \"types\": \"./dist/catalog.d.ts\",\n      \"import\": \"./dist/catalog.mjs\",\n      \"require\": \"./dist/catalog.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"embla-carousel-react\": \"^8.6.0\",\n    \"lucide-react\": \"^0.564.0\",\n    \"radix-ui\": \"^1.4.3\",\n    \"tailwind-merge\": \"^3.4.1\",\n    \"vaul\": \"^1.1.2\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"19.2.3\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"tailwindcss\": \"^4.0.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/shadcn/src/catalog.ts",
    "content": "import { z } from \"zod\";\n\n// =============================================================================\n// Shared validation schemas used across form components\n// =============================================================================\n\nconst validationCheckSchema = z\n  .array(\n    z.object({\n      type: z.string(),\n      message: z.string(),\n      args: z.record(z.string(), z.unknown()).optional(),\n    }),\n  )\n  .nullable();\n\nconst validateOnSchema = z.enum([\"change\", \"blur\", \"submit\"]).nullable();\n\n// =============================================================================\n// shadcn/ui Component Definitions\n// =============================================================================\n\n/**\n * shadcn/ui component definitions for json-render catalogs.\n *\n * These can be used directly or extended with custom components.\n * All components are built using Radix UI primitives + Tailwind CSS.\n */\nexport const shadcnComponentDefinitions = {\n  // ==========================================================================\n  // Layout Components\n  // ==========================================================================\n\n  Card: {\n    props: z.object({\n      title: z.string().nullable(),\n      description: z.string().nullable(),\n      maxWidth: z.enum([\"sm\", \"md\", \"lg\", \"full\"]).nullable(),\n      centered: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Container card for content sections. Use for forms/content boxes, NOT for page headers.\",\n    example: { title: \"Overview\", description: \"Your account summary\" },\n  },\n\n  Stack: {\n    props: z.object({\n      direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n      gap: z.enum([\"none\", \"sm\", \"md\", \"lg\"]).nullable(),\n      align: z.enum([\"start\", \"center\", \"end\", \"stretch\"]).nullable(),\n      justify: z\n        .enum([\"start\", \"center\", \"end\", \"between\", \"around\"])\n        .nullable(),\n    }),\n    slots: [\"default\"],\n    description: \"Flex container for layouts\",\n    example: { direction: \"vertical\", gap: \"md\" },\n  },\n\n  Grid: {\n    props: z.object({\n      columns: z.number().nullable(),\n      gap: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n    }),\n    slots: [\"default\"],\n    description: \"Grid layout (1-6 columns)\",\n    example: { columns: 3, gap: \"md\" },\n  },\n\n  Separator: {\n    props: z.object({\n      orientation: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n    }),\n    description: \"Visual separator line\",\n  },\n\n  Tabs: {\n    props: z.object({\n      tabs: z.array(\n        z.object({\n          label: z.string(),\n          value: z.string(),\n        }),\n      ),\n      defaultValue: z.string().nullable(),\n      value: z.string().nullable(),\n    }),\n    slots: [\"default\"],\n    events: [\"change\"],\n    description:\n      \"Tab navigation. Use { $bindState } on value for active tab binding.\",\n  },\n\n  Accordion: {\n    props: z.object({\n      items: z.array(\n        z.object({\n          title: z.string(),\n          content: z.string(),\n        }),\n      ),\n      type: z.enum([\"single\", \"multiple\"]).nullable(),\n    }),\n    description:\n      \"Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.\",\n  },\n\n  Collapsible: {\n    props: z.object({\n      title: z.string(),\n      defaultOpen: z.boolean().nullable(),\n    }),\n    slots: [\"default\"],\n    description: \"Collapsible section with trigger. Children render inside.\",\n  },\n\n  Dialog: {\n    props: z.object({\n      title: z.string(),\n      description: z.string().nullable(),\n      openPath: z.string(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Modal dialog. Set openPath to a boolean state path. Use setState to toggle.\",\n  },\n\n  Drawer: {\n    props: z.object({\n      title: z.string(),\n      description: z.string().nullable(),\n      openPath: z.string(),\n    }),\n    slots: [\"default\"],\n    description:\n      \"Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.\",\n  },\n\n  Carousel: {\n    props: z.object({\n      items: z.array(\n        z.object({\n          title: z.string().nullable(),\n          description: z.string().nullable(),\n        }),\n      ),\n    }),\n    description: \"Horizontally scrollable carousel of cards.\",\n  },\n\n  // ==========================================================================\n  // Data Display Components\n  // ==========================================================================\n\n  Table: {\n    props: z.object({\n      columns: z.array(z.string()),\n      rows: z.array(z.array(z.string())),\n      caption: z.string().nullable(),\n    }),\n    description:\n      'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [[\"Alice\",\"admin\"],[\"Bob\",\"user\"]].',\n    example: {\n      columns: [\"Name\", \"Role\"],\n      rows: [\n        [\"Alice\", \"Admin\"],\n        [\"Bob\", \"User\"],\n      ],\n    },\n  },\n\n  Heading: {\n    props: z.object({\n      text: z.string(),\n      level: z.enum([\"h1\", \"h2\", \"h3\", \"h4\"]).nullable(),\n    }),\n    description: \"Heading text (h1-h4)\",\n    example: { text: \"Welcome\", level: \"h1\" },\n  },\n\n  Text: {\n    props: z.object({\n      text: z.string(),\n      variant: z.enum([\"body\", \"caption\", \"muted\", \"lead\", \"code\"]).nullable(),\n    }),\n    description: \"Paragraph text\",\n    example: { text: \"Hello, world!\" },\n  },\n\n  Image: {\n    props: z.object({\n      src: z.string().nullable(),\n      alt: z.string(),\n      width: z.number().nullable(),\n      height: z.number().nullable(),\n    }),\n    description:\n      \"Image component. Renders an img tag when src is provided, otherwise a placeholder.\",\n  },\n\n  Avatar: {\n    props: z.object({\n      src: z.string().nullable(),\n      name: z.string(),\n      size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n    }),\n    description: \"User avatar with fallback initials\",\n    example: { name: \"Jane Doe\", size: \"md\" },\n  },\n\n  Badge: {\n    props: z.object({\n      text: z.string(),\n      variant: z\n        .enum([\"default\", \"secondary\", \"destructive\", \"outline\"])\n        .nullable(),\n    }),\n    description: \"Status badge\",\n    example: { text: \"Active\", variant: \"default\" },\n  },\n\n  Alert: {\n    props: z.object({\n      title: z.string(),\n      message: z.string().nullable(),\n      type: z.enum([\"info\", \"success\", \"warning\", \"error\"]).nullable(),\n    }),\n    description: \"Alert banner\",\n    example: {\n      title: \"Note\",\n      message: \"Your changes have been saved.\",\n      type: \"success\",\n    },\n  },\n\n  Progress: {\n    props: z.object({\n      value: z.number(),\n      max: z.number().nullable(),\n      label: z.string().nullable(),\n    }),\n    description: \"Progress bar (value 0-100)\",\n    example: { value: 65, max: 100, label: \"Upload progress\" },\n  },\n\n  Skeleton: {\n    props: z.object({\n      width: z.string().nullable(),\n      height: z.string().nullable(),\n      rounded: z.boolean().nullable(),\n    }),\n    description: \"Loading placeholder skeleton\",\n  },\n\n  Spinner: {\n    props: z.object({\n      size: z.enum([\"sm\", \"md\", \"lg\"]).nullable(),\n      label: z.string().nullable(),\n    }),\n    description: \"Loading spinner indicator\",\n  },\n\n  Tooltip: {\n    props: z.object({\n      content: z.string(),\n      text: z.string(),\n    }),\n    description: \"Hover tooltip. Shows content on hover over text.\",\n  },\n\n  Popover: {\n    props: z.object({\n      trigger: z.string(),\n      content: z.string(),\n    }),\n    description: \"Popover that appears on click of trigger.\",\n  },\n\n  // ==========================================================================\n  // Form Input Components\n  // ==========================================================================\n\n  Input: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      type: z.enum([\"text\", \"email\", \"password\", \"number\"]).nullable(),\n      placeholder: z.string().nullable(),\n      value: z.string().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    events: [\"submit\", \"focus\", \"blur\"],\n    description:\n      \"Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength). validateOn controls timing (default: blur).\",\n    example: {\n      label: \"Email\",\n      name: \"email\",\n      type: \"email\",\n      placeholder: \"you@example.com\",\n    },\n  },\n\n  Textarea: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      placeholder: z.string().nullable(),\n      rows: z.number().nullable(),\n      value: z.string().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    description:\n      \"Multi-line text input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: blur).\",\n  },\n\n  Select: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      options: z.array(z.string()),\n      placeholder: z.string().nullable(),\n      value: z.string().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    events: [\"change\"],\n    description:\n      \"Dropdown select input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).\",\n  },\n\n  Checkbox: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      checked: z.boolean().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    events: [\"change\"],\n    description:\n      \"Checkbox input. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).\",\n  },\n\n  Radio: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      options: z.array(z.string()),\n      value: z.string().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    events: [\"change\"],\n    description:\n      \"Radio button group. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).\",\n  },\n\n  Switch: {\n    props: z.object({\n      label: z.string(),\n      name: z.string(),\n      checked: z.boolean().nullable(),\n      checks: validationCheckSchema,\n      validateOn: validateOnSchema,\n    }),\n    events: [\"change\"],\n    description:\n      \"Toggle switch. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).\",\n  },\n\n  Slider: {\n    props: z.object({\n      label: z.string().nullable(),\n      min: z.number().nullable(),\n      max: z.number().nullable(),\n      step: z.number().nullable(),\n      value: z.number().nullable(),\n    }),\n    events: [\"change\"],\n    description: \"Range slider input. Use { $bindState } on value for binding.\",\n  },\n\n  // ==========================================================================\n  // Action Components\n  // ==========================================================================\n\n  Button: {\n    props: z.object({\n      label: z.string(),\n      variant: z.enum([\"primary\", \"secondary\", \"danger\"]).nullable(),\n      disabled: z.boolean().nullable(),\n    }),\n    events: [\"press\"],\n    description: \"Clickable button. Bind on.press for handler.\",\n    example: { label: \"Submit\", variant: \"primary\" },\n  },\n\n  Link: {\n    props: z.object({\n      label: z.string(),\n      href: z.string(),\n    }),\n    events: [\"press\"],\n    description: \"Anchor link. Bind on.press for click handler.\",\n  },\n\n  DropdownMenu: {\n    props: z.object({\n      label: z.string(),\n      items: z.array(\n        z.object({\n          label: z.string(),\n          value: z.string(),\n        }),\n      ),\n      value: z.string().nullable(),\n    }),\n    events: [\"select\"],\n    description:\n      \"Dropdown menu with trigger button and selectable items. Use { $bindState } on value for selected item binding.\",\n  },\n\n  Toggle: {\n    props: z.object({\n      label: z.string(),\n      pressed: z.boolean().nullable(),\n      variant: z.enum([\"default\", \"outline\"]).nullable(),\n    }),\n    events: [\"change\"],\n    description:\n      \"Toggle button. Use { $bindState } on pressed for state binding.\",\n  },\n\n  ToggleGroup: {\n    props: z.object({\n      items: z.array(\n        z.object({\n          label: z.string(),\n          value: z.string(),\n        }),\n      ),\n      type: z.enum([\"single\", \"multiple\"]).nullable(),\n      value: z.string().nullable(),\n    }),\n    events: [\"change\"],\n    description:\n      \"Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.\",\n  },\n\n  ButtonGroup: {\n    props: z.object({\n      buttons: z.array(\n        z.object({\n          label: z.string(),\n          value: z.string(),\n        }),\n      ),\n      selected: z.string().nullable(),\n    }),\n    events: [\"change\"],\n    description:\n      \"Segmented button group. Use { $bindState } on selected for selected value.\",\n  },\n\n  Pagination: {\n    props: z.object({\n      totalPages: z.number(),\n      page: z.number().nullable(),\n    }),\n    events: [\"change\"],\n    description:\n      \"Page navigation. Use { $bindState } on page for current page number.\",\n  },\n};\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Type for a component definition\n */\nexport type ComponentDefinition = {\n  props: z.ZodType;\n  slots?: string[];\n  events?: string[];\n  description: string;\n  example?: Record<string, unknown>;\n};\n\n/**\n * Infer the props type for a shadcn component by name.\n * Derives the TypeScript type directly from the Zod schema,\n * so component implementations stay in sync with catalog definitions.\n *\n * @example\n * ```ts\n * type CardProps = ShadcnProps<\"Card\">;\n * // { title: string | null; description: string | null; ... }\n * ```\n */\nexport type ShadcnProps<K extends keyof typeof shadcnComponentDefinitions> =\n  z.output<(typeof shadcnComponentDefinitions)[K][\"props\"]>;\n"
  },
  {
    "path": "packages/shadcn/src/components.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n  useBoundProp,\n  useStateBinding,\n  useFieldValidation,\n  type BaseComponentProps,\n} from \"@json-render/react\";\n\nimport { Button } from \"./ui/button\";\nimport {\n  Card,\n  CardHeader,\n  CardTitle,\n  CardDescription,\n  CardContent,\n} from \"./ui/card\";\nimport { Input } from \"./ui/input\";\nimport { Label } from \"./ui/label\";\nimport { Textarea } from \"./ui/textarea\";\nimport { Checkbox } from \"./ui/checkbox\";\nimport { Switch } from \"./ui/switch\";\nimport { Progress } from \"./ui/progress\";\nimport { Separator } from \"./ui/separator\";\nimport { Alert, AlertTitle, AlertDescription } from \"./ui/alert\";\nimport {\n  Dialog as DialogPrimitive,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"./ui/dialog\";\nimport {\n  Accordion as AccordionPrimitive,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"./ui/accordion\";\nimport {\n  Avatar as AvatarPrimitive,\n  AvatarImage,\n  AvatarFallback,\n} from \"./ui/avatar\";\nimport { Badge } from \"./ui/badge\";\nimport { RadioGroup, RadioGroupItem } from \"./ui/radio-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"./ui/select\";\nimport {\n  Carousel as CarouselPrimitive,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n} from \"./ui/carousel\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"./ui/collapsible\";\nimport {\n  Table as TablePrimitive,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"./ui/table\";\nimport {\n  Drawer as DrawerPrimitive,\n  DrawerContent,\n  DrawerDescription,\n  DrawerHeader,\n  DrawerTitle,\n} from \"./ui/drawer\";\nimport {\n  DropdownMenu as DropdownMenuPrimitive,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"./ui/dropdown-menu\";\nimport {\n  Pagination as PaginationPrimitive,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n} from \"./ui/pagination\";\nimport {\n  Popover as PopoverPrimitive,\n  PopoverContent,\n  PopoverTrigger,\n} from \"./ui/popover\";\nimport { Skeleton } from \"./ui/skeleton\";\nimport { Slider } from \"./ui/slider\";\nimport {\n  Tabs as TabsPrimitive,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n} from \"./ui/tabs\";\nimport { Toggle } from \"./ui/toggle\";\nimport { ToggleGroup, ToggleGroupItem } from \"./ui/toggle-group\";\nimport {\n  Tooltip as TooltipPrimitive,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"./ui/tooltip\";\nimport { cn } from \"./lib/utils\";\n\nimport { type ShadcnProps } from \"./catalog\";\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction getPaginationRange(\n  current: number,\n  total: number,\n): Array<number | \"ellipsis\"> {\n  if (total <= 7) {\n    return Array.from({ length: total }, (_, i) => i + 1);\n  }\n  const pages: Array<number | \"ellipsis\"> = [];\n  pages.push(1);\n  if (current > 3) {\n    pages.push(\"ellipsis\");\n  }\n  const start = Math.max(2, current - 1);\n  const end = Math.min(total - 1, current + 1);\n  for (let i = start; i <= end; i++) {\n    pages.push(i);\n  }\n  if (current < total - 2) {\n    pages.push(\"ellipsis\");\n  }\n  pages.push(total);\n  return pages;\n}\n\n// =============================================================================\n// Standard Component Implementations\n// =============================================================================\n\n/**\n * Standard shadcn/ui component implementations.\n *\n * Pass to `defineRegistry()` from `@json-render/react` to create a\n * component registry for rendering JSON specs with shadcn/ui components.\n *\n * @example\n * ```ts\n * import { defineRegistry } from \"@json-render/react\";\n * import { shadcnComponents } from \"@json-render/shadcn\";\n *\n * const { registry } = defineRegistry(catalog, {\n *   components: {\n *     Card: shadcnComponents.Card,\n *     Button: shadcnComponents.Button,\n *   },\n * });\n * ```\n */\nexport const shadcnComponents = {\n  // ── Layout ────────────────────────────────────────────────────────────\n\n  Card: ({ props, children }: BaseComponentProps<ShadcnProps<\"Card\">>) => {\n    const maxWidthClass =\n      props.maxWidth === \"sm\"\n        ? \"max-w-xs sm:min-w-[280px]\"\n        : props.maxWidth === \"md\"\n          ? \"max-w-sm sm:min-w-[320px]\"\n          : props.maxWidth === \"lg\"\n            ? \"max-w-md sm:min-w-[360px]\"\n            : \"w-full\";\n    const centeredClass = props.centered ? \"mx-auto\" : \"\";\n\n    return (\n      <Card className={cn(maxWidthClass, centeredClass)}>\n        {(props.title || props.description) && (\n          <CardHeader>\n            {props.title && <CardTitle>{props.title}</CardTitle>}\n            {props.description && (\n              <CardDescription>{props.description}</CardDescription>\n            )}\n          </CardHeader>\n        )}\n        <CardContent className=\"flex flex-col gap-3\">{children}</CardContent>\n      </Card>\n    );\n  },\n\n  Stack: ({ props, children }: BaseComponentProps<ShadcnProps<\"Stack\">>) => {\n    const isHorizontal = props.direction === \"horizontal\";\n    const gapMap: Record<string, string> = {\n      none: \"gap-0\",\n      sm: \"gap-2\",\n      md: \"gap-3\",\n      lg: \"gap-4\",\n    };\n    const alignMap: Record<string, string> = {\n      start: \"items-start\",\n      center: \"items-center\",\n      end: \"items-end\",\n      stretch: \"items-stretch\",\n    };\n    const justifyMap: Record<string, string> = {\n      start: \"\",\n      center: \"justify-center\",\n      end: \"justify-end\",\n      between: \"justify-between\",\n      around: \"justify-around\",\n    };\n\n    const gapClass = gapMap[props.gap ?? \"md\"] ?? \"gap-3\";\n    const alignClass = alignMap[props.align ?? \"start\"] ?? \"items-start\";\n    const justifyClass = justifyMap[props.justify ?? \"\"] ?? \"\";\n\n    return (\n      <div\n        className={`flex ${isHorizontal ? \"flex-row flex-wrap\" : \"flex-col\"} ${gapClass} ${alignClass} ${justifyClass}`}\n      >\n        {children}\n      </div>\n    );\n  },\n\n  Grid: ({ props, children }: BaseComponentProps<ShadcnProps<\"Grid\">>) => {\n    const colsMap: Record<number, string> = {\n      1: \"grid-cols-1\",\n      2: \"grid-cols-2\",\n      3: \"grid-cols-3\",\n      4: \"grid-cols-4\",\n      5: \"grid-cols-5\",\n      6: \"grid-cols-6\",\n    };\n    const gridGapMap: Record<string, string> = {\n      sm: \"gap-2\",\n      md: \"gap-3\",\n      lg: \"gap-4\",\n    };\n\n    const n = Math.max(1, Math.min(6, props.columns ?? 1));\n    const cols = colsMap[n] ?? \"grid-cols-1\";\n    const gridGap = gridGapMap[props.gap ?? \"md\"] ?? \"gap-3\";\n\n    return <div className={`grid ${cols} ${gridGap}`}>{children}</div>;\n  },\n\n  Separator: ({ props }: BaseComponentProps<ShadcnProps<\"Separator\">>) => {\n    return (\n      <Separator\n        orientation={props.orientation ?? \"horizontal\"}\n        className={props.orientation === \"vertical\" ? \"h-full mx-2\" : \"my-3\"}\n      />\n    );\n  },\n\n  Tabs: ({\n    props,\n    children,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Tabs\">>) => {\n    const tabs = props.tabs ?? [];\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(\n      props.defaultValue ?? tabs[0]?.value ?? \"\",\n    );\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? tabs[0]?.value ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n\n    return (\n      <TabsPrimitive\n        value={value}\n        onValueChange={(v) => {\n          setValue(v);\n          emit(\"change\");\n        }}\n      >\n        <TabsList>\n          {tabs.map((tab) => (\n            <TabsTrigger key={tab.value} value={tab.value}>\n              {tab.label}\n            </TabsTrigger>\n          ))}\n        </TabsList>\n        {children}\n      </TabsPrimitive>\n    );\n  },\n\n  Accordion: ({ props }: BaseComponentProps<ShadcnProps<\"Accordion\">>) => {\n    const items = props.items ?? [];\n    const isMultiple = props.type === \"multiple\";\n\n    const itemElements = items.map((item, i) => (\n      <AccordionItem key={i} value={`item-${i}`}>\n        <AccordionTrigger>{item.title}</AccordionTrigger>\n        <AccordionContent>{item.content}</AccordionContent>\n      </AccordionItem>\n    ));\n\n    if (isMultiple) {\n      return (\n        <AccordionPrimitive type=\"multiple\" className=\"w-full\">\n          {itemElements}\n        </AccordionPrimitive>\n      );\n    }\n    return (\n      <AccordionPrimitive type=\"single\" collapsible className=\"w-full\">\n        {itemElements}\n      </AccordionPrimitive>\n    );\n  },\n\n  Collapsible: ({\n    props,\n    children,\n  }: BaseComponentProps<ShadcnProps<\"Collapsible\">>) => {\n    const [open, setOpen] = useState(props.defaultOpen ?? false);\n    return (\n      <Collapsible open={open} onOpenChange={setOpen} className=\"w-full\">\n        <CollapsibleTrigger asChild>\n          <button className=\"flex w-full items-center justify-between rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-muted transition-colors\">\n            {props.title}\n            <svg\n              className={`h-4 w-4 transition-transform ${open ? \"rotate-180\" : \"\"}`}\n              fill=\"none\"\n              viewBox=\"0 0 24 24\"\n              stroke=\"currentColor\"\n              strokeWidth={2}\n            >\n              <path\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                d=\"M19 9l-7 7-7-7\"\n              />\n            </svg>\n          </button>\n        </CollapsibleTrigger>\n        <CollapsibleContent className=\"pt-2\">{children}</CollapsibleContent>\n      </Collapsible>\n    );\n  },\n\n  Dialog: ({ props, children }: BaseComponentProps<ShadcnProps<\"Dialog\">>) => {\n    const [open, setOpen] = useStateBinding<boolean>(props.openPath ?? \"\");\n    return (\n      <DialogPrimitive open={open ?? false} onOpenChange={(v) => setOpen(v)}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>{props.title}</DialogTitle>\n            {props.description && (\n              <DialogDescription>{props.description}</DialogDescription>\n            )}\n          </DialogHeader>\n          {children}\n        </DialogContent>\n      </DialogPrimitive>\n    );\n  },\n\n  Drawer: ({ props, children }: BaseComponentProps<ShadcnProps<\"Drawer\">>) => {\n    const [open, setOpen] = useStateBinding<boolean>(props.openPath ?? \"\");\n    return (\n      <DrawerPrimitive open={open ?? false} onOpenChange={(v) => setOpen(v)}>\n        <DrawerContent>\n          <DrawerHeader>\n            <DrawerTitle>{props.title}</DrawerTitle>\n            {props.description && (\n              <DrawerDescription>{props.description}</DrawerDescription>\n            )}\n          </DrawerHeader>\n          <div className=\"p-4\">{children}</div>\n        </DrawerContent>\n      </DrawerPrimitive>\n    );\n  },\n\n  Carousel: ({ props }: BaseComponentProps<ShadcnProps<\"Carousel\">>) => {\n    const items = props.items ?? [];\n    return (\n      <CarouselPrimitive className=\"w-full\">\n        <CarouselContent>\n          {items.map((item, i) => (\n            <CarouselItem\n              key={i}\n              className=\"basis-3/4 md:basis-1/2 lg:basis-1/3\"\n            >\n              <div className=\"border border-border rounded-lg p-4 bg-card h-full\">\n                {item.title && (\n                  <h4 className=\"font-semibold text-sm mb-1\">{item.title}</h4>\n                )}\n                {item.description && (\n                  <p className=\"text-sm text-muted-foreground\">\n                    {item.description}\n                  </p>\n                )}\n              </div>\n            </CarouselItem>\n          ))}\n        </CarouselContent>\n        <CarouselPrevious />\n        <CarouselNext />\n      </CarouselPrimitive>\n    );\n  },\n\n  // ── Data Display ──────────────────────────────────────────────────────\n\n  Table: ({ props }: BaseComponentProps<ShadcnProps<\"Table\">>) => {\n    const columns = props.columns ?? [];\n    const rows = (props.rows ?? []).map((row) => row.map(String));\n\n    return (\n      <div className=\"rounded-md border border-border overflow-hidden\">\n        <TablePrimitive>\n          {props.caption && <TableCaption>{props.caption}</TableCaption>}\n          <TableHeader>\n            <TableRow>\n              {columns.map((col) => (\n                <TableHead key={col}>{col}</TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {rows.map((row, i) => (\n              <TableRow key={i}>\n                {row.map((cell, j) => (\n                  <TableCell key={j}>{cell}</TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </TablePrimitive>\n      </div>\n    );\n  },\n\n  Heading: ({ props }: BaseComponentProps<ShadcnProps<\"Heading\">>) => {\n    const level = props.level ?? \"h2\";\n    const headingClass =\n      level === \"h1\"\n        ? \"text-2xl font-bold\"\n        : level === \"h3\"\n          ? \"text-base font-semibold\"\n          : level === \"h4\"\n            ? \"text-sm font-semibold\"\n            : \"text-lg font-semibold\";\n\n    if (level === \"h1\")\n      return <h1 className={`${headingClass} text-left`}>{props.text}</h1>;\n    if (level === \"h3\")\n      return <h3 className={`${headingClass} text-left`}>{props.text}</h3>;\n    if (level === \"h4\")\n      return <h4 className={`${headingClass} text-left`}>{props.text}</h4>;\n    return <h2 className={`${headingClass} text-left`}>{props.text}</h2>;\n  },\n\n  Text: ({ props }: BaseComponentProps<ShadcnProps<\"Text\">>) => {\n    const textClass =\n      props.variant === \"caption\"\n        ? \"text-xs\"\n        : props.variant === \"muted\"\n          ? \"text-sm text-muted-foreground\"\n          : props.variant === \"lead\"\n            ? \"text-xl text-muted-foreground\"\n            : props.variant === \"code\"\n              ? \"font-mono text-sm bg-muted px-1.5 py-0.5 rounded\"\n              : \"text-sm\";\n\n    if (props.variant === \"code\") {\n      return <code className={`${textClass} text-left`}>{props.text}</code>;\n    }\n    return <p className={`${textClass} text-left`}>{props.text}</p>;\n  },\n\n  Image: ({ props }: BaseComponentProps<ShadcnProps<\"Image\">>) => {\n    if (props.src) {\n      return (\n        <img\n          src={props.src}\n          alt={props.alt ?? \"\"}\n          width={props.width ?? undefined}\n          height={props.height ?? undefined}\n          className=\"rounded max-w-full\"\n        />\n      );\n    }\n    return (\n      <div\n        className=\"bg-muted border border-border rounded flex items-center justify-center text-xs text-muted-foreground\"\n        style={{ width: props.width ?? 80, height: props.height ?? 60 }}\n      >\n        {props.alt || \"img\"}\n      </div>\n    );\n  },\n\n  Avatar: ({ props }: BaseComponentProps<ShadcnProps<\"Avatar\">>) => {\n    const name = props.name || \"?\";\n    const initials = name\n      .split(\" \")\n      .map((n) => n[0])\n      .join(\"\")\n      .slice(0, 2)\n      .toUpperCase();\n    const sizeClass =\n      props.size === \"lg\"\n        ? \"h-12 w-12\"\n        : props.size === \"sm\"\n          ? \"h-8 w-8\"\n          : \"h-10 w-10\";\n\n    return (\n      <AvatarPrimitive className={sizeClass}>\n        {props.src && <AvatarImage src={props.src} alt={name} />}\n        <AvatarFallback>{initials}</AvatarFallback>\n      </AvatarPrimitive>\n    );\n  },\n\n  Badge: ({ props }: BaseComponentProps<ShadcnProps<\"Badge\">>) => {\n    return <Badge variant={props.variant ?? \"default\"}>{props.text}</Badge>;\n  },\n\n  Alert: ({ props }: BaseComponentProps<ShadcnProps<\"Alert\">>) => {\n    const variant = props.type === \"error\" ? \"destructive\" : \"default\";\n    const customClass =\n      props.type === \"success\"\n        ? \"border-green-200 bg-green-50 text-green-900 dark:border-green-800 dark:bg-green-950 dark:text-green-100\"\n        : props.type === \"warning\"\n          ? \"border-yellow-200 bg-yellow-50 text-yellow-900 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-100\"\n          : props.type === \"info\"\n            ? \"border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100\"\n            : \"\";\n\n    return (\n      <Alert variant={variant} className={customClass}>\n        <AlertTitle>{props.title}</AlertTitle>\n        {props.message && <AlertDescription>{props.message}</AlertDescription>}\n      </Alert>\n    );\n  },\n\n  Progress: ({ props }: BaseComponentProps<ShadcnProps<\"Progress\">>) => {\n    const value = Math.min(100, Math.max(0, props.value || 0));\n    return (\n      <div className=\"space-y-2\">\n        {props.label && (\n          <Label className=\"text-sm text-muted-foreground\">{props.label}</Label>\n        )}\n        <Progress value={value} />\n      </div>\n    );\n  },\n\n  Skeleton: ({ props }: BaseComponentProps<ShadcnProps<\"Skeleton\">>) => {\n    return (\n      <Skeleton\n        className={props.rounded ? \"rounded-full\" : \"rounded-md\"}\n        style={{\n          width: props.width ?? \"100%\",\n          height: props.height ?? \"1.25rem\",\n        }}\n      />\n    );\n  },\n\n  Spinner: ({ props }: BaseComponentProps<ShadcnProps<\"Spinner\">>) => {\n    const sizeClass =\n      props.size === \"lg\"\n        ? \"h-8 w-8\"\n        : props.size === \"sm\"\n          ? \"h-4 w-4\"\n          : \"h-6 w-6\";\n    return (\n      <div className=\"flex items-center gap-2\">\n        <svg\n          className={`${sizeClass} animate-spin text-muted-foreground`}\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n        >\n          <circle\n            className=\"opacity-25\"\n            cx=\"12\"\n            cy=\"12\"\n            r=\"10\"\n            stroke=\"currentColor\"\n            strokeWidth=\"4\"\n          />\n          <path\n            className=\"opacity-75\"\n            fill=\"currentColor\"\n            d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"\n          />\n        </svg>\n        {props.label && (\n          <span className=\"text-sm text-muted-foreground\">{props.label}</span>\n        )}\n      </div>\n    );\n  },\n\n  Tooltip: ({ props }: BaseComponentProps<ShadcnProps<\"Tooltip\">>) => {\n    return (\n      <TooltipProvider>\n        <TooltipPrimitive>\n          <TooltipTrigger asChild>\n            <span className=\"text-sm underline decoration-dotted cursor-help\">\n              {props.text}\n            </span>\n          </TooltipTrigger>\n          <TooltipContent>\n            <p>{props.content}</p>\n          </TooltipContent>\n        </TooltipPrimitive>\n      </TooltipProvider>\n    );\n  },\n\n  Popover: ({ props }: BaseComponentProps<ShadcnProps<\"Popover\">>) => {\n    return (\n      <PopoverPrimitive>\n        <PopoverTrigger asChild>\n          <Button variant=\"outline\" className=\"text-sm\">\n            {props.trigger}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent className=\"w-64\">\n          <p className=\"text-sm\">{props.content}</p>\n        </PopoverContent>\n      </PopoverPrimitive>\n    );\n  },\n\n  // ── Form Inputs ───────────────────────────────────────────────────────\n\n  Input: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Input\">>) => {\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(\"\");\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n    const validateOn = props.validateOn ?? \"blur\";\n\n    const hasValidation = !!(bindings?.value && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.value ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-2\">\n        {props.label && (\n          <Label htmlFor={props.name ?? undefined}>{props.label}</Label>\n        )}\n        <Input\n          id={props.name ?? undefined}\n          name={props.name ?? undefined}\n          type={props.type ?? \"text\"}\n          placeholder={props.placeholder ?? \"\"}\n          value={value}\n          onChange={(e) => {\n            setValue(e.target.value);\n            if (hasValidation && validateOn === \"change\") validate();\n          }}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") emit(\"submit\");\n          }}\n          onFocus={() => emit(\"focus\")}\n          onBlur={() => {\n            if (hasValidation && validateOn === \"blur\") validate();\n            emit(\"blur\");\n          }}\n        />\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Textarea: ({\n    props,\n    bindings,\n  }: BaseComponentProps<ShadcnProps<\"Textarea\">>) => {\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(\"\");\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n    const validateOn = props.validateOn ?? \"blur\";\n\n    const hasValidation = !!(bindings?.value && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.value ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-2\">\n        {props.label && (\n          <Label htmlFor={props.name ?? undefined}>{props.label}</Label>\n        )}\n        <Textarea\n          id={props.name ?? undefined}\n          name={props.name ?? undefined}\n          placeholder={props.placeholder ?? \"\"}\n          rows={props.rows ?? 3}\n          value={value}\n          onChange={(e) => {\n            setValue(e.target.value);\n            if (hasValidation && validateOn === \"change\") validate();\n          }}\n          onBlur={() => {\n            if (hasValidation && validateOn === \"blur\") validate();\n          }}\n        />\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Select: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Select\">>) => {\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState<string>(\"\");\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n    const rawOptions = props.options ?? [];\n    const options = rawOptions.map((opt) =>\n      typeof opt === \"string\" ? opt : String(opt ?? \"\"),\n    );\n    const validateOn = props.validateOn ?? \"change\";\n\n    const hasValidation = !!(bindings?.value && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.value ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-2\">\n        <Label>{props.label}</Label>\n        <Select\n          value={value}\n          onValueChange={(v) => {\n            setValue(v);\n            // Select has no native blur event, so only validate on \"change\"\n            if (hasValidation && validateOn === \"change\") validate();\n            emit(\"change\");\n          }}\n        >\n          <SelectTrigger className=\"w-full\">\n            <SelectValue placeholder={props.placeholder ?? \"Select...\"} />\n          </SelectTrigger>\n          <SelectContent>\n            {options.map((opt, idx) => (\n              <SelectItem key={`${idx}-${opt}`} value={opt || `option-${idx}`}>\n                {opt}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Checkbox: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Checkbox\">>) => {\n    const [boundChecked, setBoundChecked] = useBoundProp<boolean>(\n      props.checked as boolean | undefined,\n      bindings?.checked,\n    );\n    const [localChecked, setLocalChecked] = useState(!!props.checked);\n    const isBound = !!bindings?.checked;\n    const checked = isBound ? (boundChecked ?? false) : localChecked;\n    const setChecked = isBound ? setBoundChecked : setLocalChecked;\n\n    const validateOn = props.validateOn ?? \"change\";\n    const hasValidation = !!(bindings?.checked && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.checked ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-1\">\n        <div className=\"flex items-center space-x-2\">\n          <Checkbox\n            id={props.name ?? undefined}\n            checked={checked}\n            onCheckedChange={(c) => {\n              setChecked(c === true);\n              if (hasValidation && validateOn === \"change\") validate();\n              emit(\"change\");\n            }}\n          />\n          <Label htmlFor={props.name ?? undefined} className=\"cursor-pointer\">\n            {props.label}\n          </Label>\n        </div>\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Radio: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Radio\">>) => {\n    const rawOptions = props.options ?? [];\n    const options = rawOptions.map((opt) =>\n      typeof opt === \"string\" ? opt : String(opt ?? \"\"),\n    );\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(options[0] ?? \"\");\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n\n    const validateOn = props.validateOn ?? \"change\";\n    const hasValidation = !!(bindings?.value && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.value ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-2\">\n        {props.label && <Label>{props.label}</Label>}\n        <RadioGroup\n          value={value}\n          onValueChange={(v) => {\n            setValue(v);\n            if (hasValidation && validateOn === \"change\") validate();\n            emit(\"change\");\n          }}\n        >\n          {options.map((opt, idx) => (\n            <div key={`${idx}-${opt}`} className=\"flex items-center space-x-2\">\n              <RadioGroupItem\n                value={opt || `option-${idx}`}\n                id={`${props.name}-${idx}-${opt}`}\n              />\n              <Label\n                htmlFor={`${props.name}-${idx}-${opt}`}\n                className=\"cursor-pointer\"\n              >\n                {opt}\n              </Label>\n            </div>\n          ))}\n        </RadioGroup>\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Switch: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Switch\">>) => {\n    const [boundChecked, setBoundChecked] = useBoundProp<boolean>(\n      props.checked as boolean | undefined,\n      bindings?.checked,\n    );\n    const [localChecked, setLocalChecked] = useState(!!props.checked);\n    const isBound = !!bindings?.checked;\n    const checked = isBound ? (boundChecked ?? false) : localChecked;\n    const setChecked = isBound ? setBoundChecked : setLocalChecked;\n\n    const validateOn = props.validateOn ?? \"change\";\n    const hasValidation = !!(bindings?.checked && props.checks?.length);\n    const { errors, validate } = useFieldValidation(\n      bindings?.checked ?? \"\",\n      hasValidation ? { checks: props.checks ?? [], validateOn } : undefined,\n    );\n\n    return (\n      <div className=\"space-y-1\">\n        <div className=\"flex items-center justify-between space-x-2\">\n          <Label htmlFor={props.name ?? undefined} className=\"cursor-pointer\">\n            {props.label}\n          </Label>\n          <Switch\n            id={props.name ?? undefined}\n            checked={checked}\n            onCheckedChange={(c) => {\n              setChecked(c);\n              if (hasValidation && validateOn === \"change\") validate();\n              emit(\"change\");\n            }}\n          />\n        </div>\n        {errors.length > 0 && (\n          <p className=\"text-sm text-destructive\">{errors[0]}</p>\n        )}\n      </div>\n    );\n  },\n\n  Slider: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Slider\">>) => {\n    const [boundValue, setBoundValue] = useBoundProp<number>(\n      props.value as number | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(props.min ?? 0);\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? props.min ?? 0) : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n\n    return (\n      <div className=\"space-y-2\">\n        {props.label && (\n          <div className=\"flex justify-between\">\n            <Label className=\"text-sm\">{props.label}</Label>\n            <span className=\"text-sm text-muted-foreground\">{value}</span>\n          </div>\n        )}\n        <Slider\n          value={[value]}\n          min={props.min ?? 0}\n          max={props.max ?? 100}\n          step={props.step ?? 1}\n          onValueChange={(v) => {\n            setValue(v[0] ?? 0);\n            emit(\"change\");\n          }}\n        />\n      </div>\n    );\n  },\n\n  // ── Actions ───────────────────────────────────────────────────────────\n\n  Button: ({ props, emit }: BaseComponentProps<ShadcnProps<\"Button\">>) => {\n    const variant =\n      props.variant === \"danger\"\n        ? \"destructive\"\n        : props.variant === \"secondary\"\n          ? \"secondary\"\n          : \"default\";\n\n    return (\n      <Button\n        variant={variant}\n        disabled={props.disabled ?? false}\n        onClick={() => emit(\"press\")}\n      >\n        {props.label}\n      </Button>\n    );\n  },\n\n  Link: ({ props, on }: BaseComponentProps<ShadcnProps<\"Link\">>) => {\n    return (\n      <a\n        href={props.href ?? \"#\"}\n        className=\"text-primary underline-offset-4 hover:underline text-sm font-medium\"\n        onClick={(e) => {\n          const press = on(\"press\");\n          if (press.shouldPreventDefault) e.preventDefault();\n          press.emit();\n        }}\n      >\n        {props.label}\n      </a>\n    );\n  },\n\n  DropdownMenu: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"DropdownMenu\">>) => {\n    const items = props.items ?? [];\n    const [, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    return (\n      <DropdownMenuPrimitive>\n        <DropdownMenuTrigger asChild>\n          <Button variant=\"outline\">{props.label}</Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent>\n          {items.map((item) => (\n            <DropdownMenuItem\n              key={item.value}\n              onClick={() => {\n                setBoundValue(item.value);\n                emit(\"select\");\n              }}\n            >\n              {item.label}\n            </DropdownMenuItem>\n          ))}\n        </DropdownMenuContent>\n      </DropdownMenuPrimitive>\n    );\n  },\n\n  Toggle: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Toggle\">>) => {\n    const [boundPressed, setBoundPressed] = useBoundProp<boolean>(\n      props.pressed as boolean | undefined,\n      bindings?.pressed,\n    );\n    const [localPressed, setLocalPressed] = useState(props.pressed ?? false);\n    const isBound = !!bindings?.pressed;\n    const pressed = isBound ? (boundPressed ?? false) : localPressed;\n    const setPressed = isBound ? setBoundPressed : setLocalPressed;\n\n    return (\n      <Toggle\n        variant={props.variant ?? \"default\"}\n        pressed={pressed}\n        onPressedChange={(v) => {\n          setPressed(v);\n          emit(\"change\");\n        }}\n      >\n        {props.label}\n      </Toggle>\n    );\n  },\n\n  ToggleGroup: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"ToggleGroup\">>) => {\n    const type = props.type ?? \"single\";\n    const items = props.items ?? [];\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      props.value as string | undefined,\n      bindings?.value,\n    );\n    const [localValue, setLocalValue] = useState(items[0]?.value ?? \"\");\n    const isBound = !!bindings?.value;\n    const value = isBound ? (boundValue ?? \"\") : localValue;\n    const setValue = isBound ? setBoundValue : setLocalValue;\n\n    if (type === \"multiple\") {\n      const selected = value ? value.split(\",\").filter(Boolean) : [];\n      return (\n        <ToggleGroup\n          type=\"multiple\"\n          value={selected}\n          onValueChange={(v) => {\n            setValue(v.join(\",\"));\n            emit(\"change\");\n          }}\n        >\n          {items.map((item) => (\n            <ToggleGroupItem key={item.value} value={item.value}>\n              {item.label}\n            </ToggleGroupItem>\n          ))}\n        </ToggleGroup>\n      );\n    }\n\n    return (\n      <ToggleGroup\n        type=\"single\"\n        value={value}\n        onValueChange={(v) => {\n          if (v) {\n            setValue(v);\n            emit(\"change\");\n          }\n        }}\n      >\n        {items.map((item) => (\n          <ToggleGroupItem key={item.value} value={item.value}>\n            {item.label}\n          </ToggleGroupItem>\n        ))}\n      </ToggleGroup>\n    );\n  },\n\n  ButtonGroup: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"ButtonGroup\">>) => {\n    const buttons = props.buttons ?? [];\n    const [boundSelected, setBoundSelected] = useBoundProp<string>(\n      props.selected as string | undefined,\n      bindings?.selected,\n    );\n    const [localValue, setLocalValue] = useState(buttons[0]?.value ?? \"\");\n    const isBound = !!bindings?.selected;\n    const value = isBound ? (boundSelected ?? \"\") : localValue;\n    const setValue = isBound ? setBoundSelected : setLocalValue;\n\n    return (\n      <div className=\"inline-flex rounded-md border border-border\">\n        {buttons.map((btn, i) => (\n          <button\n            key={btn.value}\n            className={`px-3 py-1.5 text-sm transition-colors ${\n              value === btn.value\n                ? \"bg-primary text-primary-foreground\"\n                : \"bg-background hover:bg-muted\"\n            } ${i > 0 ? \"border-l border-border\" : \"\"} ${\n              i === 0 ? \"rounded-l-md\" : \"\"\n            } ${i === buttons.length - 1 ? \"rounded-r-md\" : \"\"}`}\n            onClick={() => {\n              setValue(btn.value);\n              emit(\"change\");\n            }}\n          >\n            {btn.label}\n          </button>\n        ))}\n      </div>\n    );\n  },\n\n  Pagination: ({\n    props,\n    bindings,\n    emit,\n  }: BaseComponentProps<ShadcnProps<\"Pagination\">>) => {\n    const [boundPage, setBoundPage] = useBoundProp<number>(\n      props.page as number | undefined,\n      bindings?.page,\n    );\n    const currentPage = boundPage ?? 1;\n    const totalPages = props.totalPages ?? 1;\n    const pages = getPaginationRange(currentPage, totalPages);\n\n    return (\n      <PaginationPrimitive>\n        <PaginationContent>\n          <PaginationItem>\n            <PaginationPrevious\n              href=\"#\"\n              onClick={(e) => {\n                e.preventDefault();\n                if (currentPage > 1) {\n                  setBoundPage(currentPage - 1);\n                  emit(\"change\");\n                }\n              }}\n            />\n          </PaginationItem>\n          {pages.map((page, idx) =>\n            page === \"ellipsis\" ? (\n              <PaginationItem key={`ellipsis-${idx}`}>\n                <PaginationEllipsis />\n              </PaginationItem>\n            ) : (\n              <PaginationItem key={page}>\n                <PaginationLink\n                  href=\"#\"\n                  isActive={page === currentPage}\n                  onClick={(e) => {\n                    e.preventDefault();\n                    setBoundPage(page);\n                    emit(\"change\");\n                  }}\n                >\n                  {page}\n                </PaginationLink>\n              </PaginationItem>\n            ),\n          )}\n          <PaginationItem>\n            <PaginationNext\n              href=\"#\"\n              onClick={(e) => {\n                e.preventDefault();\n                if (currentPage < totalPages) {\n                  setBoundPage(currentPage + 1);\n                  emit(\"change\");\n                }\n              }}\n            />\n          </PaginationItem>\n        </PaginationContent>\n      </PaginationPrimitive>\n    );\n  },\n};\n"
  },
  {
    "path": "packages/shadcn/src/index.ts",
    "content": "// Component implementations\nexport { shadcnComponents } from \"./components\";\n\n// Catalog definitions (also available via @json-render/shadcn/catalog)\nexport {\n  shadcnComponentDefinitions,\n  type ComponentDefinition,\n  type ShadcnProps,\n} from \"./catalog\";\n"
  },
  {
    "path": "packages/shadcn/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "packages/shadcn/src/ui/accordion.tsx",
    "content": "import * as React from \"react\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { Accordion as AccordionPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Accordion({\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n  return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />;\n}\n\nfunction AccordionItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n  return (\n    <AccordionPrimitive.Item\n      data-slot=\"accordion-item\"\n      className={cn(\"border-b last:border-b-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AccordionTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n  return (\n    <AccordionPrimitive.Header className=\"flex\">\n      <AccordionPrimitive.Trigger\n        data-slot=\"accordion-trigger\"\n        className={cn(\n          \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n      </AccordionPrimitive.Trigger>\n    </AccordionPrimitive.Header>\n  );\n}\n\nfunction AccordionContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n  return (\n    <AccordionPrimitive.Content\n      data-slot=\"accordion-content\"\n      className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n      {...props}\n    >\n      <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n    </AccordionPrimitive.Content>\n  );\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n"
  },
  {
    "path": "packages/shadcn/src/ui/alert.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"../lib/utils\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        destructive:\n          \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Alert({\n  className,\n  variant,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n  return (\n    <div\n      data-slot=\"alert\"\n      role=\"alert\"\n      className={cn(alertVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-title\"\n      className={cn(\n        \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AlertDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"alert-description\"\n      className={cn(\n        \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "packages/shadcn/src/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Avatar({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n  return (\n    <AvatarPrimitive.Root\n      data-slot=\"avatar\"\n      className={cn(\n        \"relative flex size-10 shrink-0 overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarImage({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n  return (\n    <AvatarPrimitive.Image\n      data-slot=\"avatar-image\"\n      className={cn(\"aspect-square size-full\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction AvatarFallback({\n  className,\n  ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n  return (\n    <AvatarPrimitive.Fallback\n      data-slot=\"avatar-fallback\"\n      className={cn(\n        \"bg-muted flex size-full items-center justify-center rounded-full\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "packages/shadcn/src/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n        secondary:\n          \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n        destructive:\n          \"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction Badge({\n  className,\n  variant = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"span\"> &\n  VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n  const Comp = asChild ? Slot.Root : \"span\";\n\n  return (\n    <Comp\n      data-slot=\"badge\"\n      data-variant={variant}\n      className={cn(badgeVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "packages/shadcn/src/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-8\",\n        \"icon-lg\": \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "packages/shadcn/src/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card\"\n      className={cn(\n        \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-content\"\n      className={cn(\"px-6\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/carousel.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-carousel-react\";\nimport { ArrowLeft, ArrowRight } from \"lucide-react\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"./button\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\");\n  }\n\n  return context;\n}\n\nfunction Carousel({\n  orientation = \"horizontal\",\n  opts,\n  setApi,\n  plugins,\n  className,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n  const [carouselRef, api] = useEmblaCarousel(\n    {\n      ...opts,\n      axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n    },\n    plugins,\n  );\n  const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n  const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n  const onSelect = React.useCallback((api: CarouselApi) => {\n    if (!api) return;\n    setCanScrollPrev(api.canScrollPrev());\n    setCanScrollNext(api.canScrollNext());\n  }, []);\n\n  const scrollPrev = React.useCallback(() => {\n    api?.scrollPrev();\n  }, [api]);\n\n  const scrollNext = React.useCallback(() => {\n    api?.scrollNext();\n  }, [api]);\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        scrollPrev();\n      } else if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        scrollNext();\n      }\n    },\n    [scrollPrev, scrollNext],\n  );\n\n  React.useEffect(() => {\n    if (!api || !setApi) return;\n    setApi(api);\n  }, [api, setApi]);\n\n  React.useEffect(() => {\n    if (!api) return;\n    onSelect(api);\n    api.on(\"reInit\", onSelect);\n    api.on(\"select\", onSelect);\n\n    return () => {\n      api?.off(\"select\", onSelect);\n    };\n  }, [api, onSelect]);\n\n  return (\n    <CarouselContext.Provider\n      value={{\n        carouselRef,\n        api: api,\n        opts,\n        orientation:\n          orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n      }}\n    >\n      <div\n        onKeyDownCapture={handleKeyDown}\n        className={cn(\"relative\", className)}\n        role=\"region\"\n        aria-roledescription=\"carousel\"\n        data-slot=\"carousel\"\n        {...props}\n      >\n        {children}\n      </div>\n    </CarouselContext.Provider>\n  );\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { carouselRef, orientation } = useCarousel();\n\n  return (\n    <div\n      ref={carouselRef}\n      className=\"overflow-hidden\"\n      data-slot=\"carousel-content\"\n    >\n      <div\n        className={cn(\n          \"flex\",\n          orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n  const { orientation } = useCarousel();\n\n  return (\n    <div\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      data-slot=\"carousel-item\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full\",\n        orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CarouselPrevious({\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-previous\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute size-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 -left-12 -translate-y-1/2\"\n          : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      {...props}\n    >\n      <ArrowLeft />\n      <span className=\"sr-only\">Previous slide</span>\n    </Button>\n  );\n}\n\nfunction CarouselNext({\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n  return (\n    <Button\n      data-slot=\"carousel-next\"\n      variant={variant}\n      size={size}\n      className={cn(\n        \"absolute size-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"top-1/2 -right-12 -translate-y-1/2\"\n          : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      {...props}\n    >\n      <ArrowRight />\n      <span className=\"sr-only\">Next slide</span>\n    </Button>\n  );\n}\n\nexport {\n  type CarouselApi,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/checkbox.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Checkbox({\n  className,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      className={cn(\n        \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <CheckboxPrimitive.Indicator\n        data-slot=\"checkbox-indicator\"\n        className=\"grid place-content-center text-current transition-none\"\n      >\n        <CheckIcon className=\"size-3.5\" />\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox };\n"
  },
  {
    "path": "packages/shadcn/src/ui/collapsible.tsx",
    "content": "import * as React from \"react\";\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\n\nfunction Collapsible({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n  return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />;\n}\n\nfunction CollapsibleTrigger({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleTrigger\n      data-slot=\"collapsible-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction CollapsibleContent({\n  ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n  return (\n    <CollapsiblePrimitive.CollapsibleContent\n      data-slot=\"collapsible-content\"\n      {...props}\n    />\n  );\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n"
  },
  {
    "path": "packages/shadcn/src/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"./button\";\n\nfunction Dialog({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n  return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n  return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n  return (\n    <DialogPrimitive.Overlay\n      data-slot=\"dialog-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <DialogPortal data-slot=\"dialog-portal\">\n      <DialogOverlay />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close\n            data-slot=\"dialog-close\"\n            className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n          >\n            <XIcon />\n            <span className=\"sr-only\">Close</span>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPortal>\n  );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"dialog-header\"\n      className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogFooter({\n  className,\n  showCloseButton = false,\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  showCloseButton?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"dialog-footer\"\n      className={cn(\n        \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {showCloseButton && (\n        <DialogPrimitive.Close asChild>\n          <Button variant=\"outline\">Close</Button>\n        </DialogPrimitive.Close>\n      )}\n    </div>\n  );\n}\n\nfunction DialogTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"dialog-title\"\n      className={cn(\"text-lg leading-none font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DialogDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"dialog-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,\n  DialogPortal,\n  DialogTitle,\n  DialogTrigger,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/drawer.tsx",
    "content": "import * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Drawer({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n  return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />;\n}\n\nfunction DrawerTrigger({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return (\n    <DrawerPrimitive.Overlay\n      data-slot=\"drawer-overlay\"\n      className={cn(\n        \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerContent({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  return (\n    <DrawerPortal data-slot=\"drawer-portal\">\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        data-slot=\"drawer-content\"\n        className={cn(\n          \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n          \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n          \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n          \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n          \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n        {children}\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  );\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\n        \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerTitle({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return (\n    <DrawerPrimitive.Title\n      data-slot=\"drawer-title\"\n      className={cn(\"text-foreground font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return (\n    <DrawerPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Drawer,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n  return (\n    <input\n      type={type}\n      data-slot=\"input\"\n      className={cn(\n        \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n        \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
  },
  {
    "path": "packages/shadcn/src/ui/label.tsx",
    "content": "import * as React from \"react\";\nimport { Label as LabelPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Label({\n  className,\n  ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n  return (\n    <LabelPrimitive.Root\n      data-slot=\"label\"\n      className={cn(\n        \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Label };\n"
  },
  {
    "path": "packages/shadcn/src/ui/pagination.tsx",
    "content": "import * as React from \"react\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MoreHorizontalIcon,\n} from \"lucide-react\";\n\nimport { cn } from \"../lib/utils\";\nimport { buttonVariants, type Button } from \"./button\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n  return (\n    <nav\n      role=\"navigation\"\n      aria-label=\"pagination\"\n      data-slot=\"pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationContent({\n  className,\n  ...props\n}: React.ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"pagination-content\"\n      className={cn(\"flex flex-row items-center gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n  return <li data-slot=\"pagination-item\" {...props} />;\n}\n\ntype PaginationLinkProps = {\n  isActive?: boolean;\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n  React.ComponentProps<\"a\">;\n\nfunction PaginationLink({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) {\n  return (\n    <a\n      aria-current={isActive ? \"page\" : undefined}\n      data-slot=\"pagination-link\"\n      data-active={isActive}\n      className={cn(\n        buttonVariants({\n          variant: isActive ? \"outline\" : \"ghost\",\n          size,\n        }),\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction PaginationPrevious({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to previous page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pl-2.5\", className)}\n      {...props}\n    >\n      <ChevronLeftIcon />\n      <span className=\"hidden sm:block\">Previous</span>\n    </PaginationLink>\n  );\n}\n\nfunction PaginationNext({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n  return (\n    <PaginationLink\n      aria-label=\"Go to next page\"\n      size=\"default\"\n      className={cn(\"gap-1 px-2.5 sm:pr-2.5\", className)}\n      {...props}\n    >\n      <span className=\"hidden sm:block\">Next</span>\n      <ChevronRightIcon />\n    </PaginationLink>\n  );\n}\n\nfunction PaginationEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"pagination-ellipsis\"\n      className={cn(\"flex size-9 items-center justify-center\", className)}\n      {...props}\n    >\n      <MoreHorizontalIcon className=\"size-4\" />\n      <span className=\"sr-only\">More pages</span>\n    </span>\n  );\n}\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Popover({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n  return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />;\n}\n\nfunction PopoverTrigger({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n  return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />;\n}\n\nfunction PopoverContent({\n  className,\n  align = \"center\",\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        data-slot=\"popover-content\"\n        align={align}\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n}\n\nfunction PopoverAnchor({\n  ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n  return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />;\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"popover-header\"\n      className={cn(\"flex flex-col gap-1 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n  return (\n    <div\n      data-slot=\"popover-title\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction PopoverDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"popover-description\"\n      className={cn(\"text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverAnchor,\n  PopoverHeader,\n  PopoverTitle,\n  PopoverDescription,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/progress.tsx",
    "content": "import * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Progress({\n  className,\n  value,\n  ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n  return (\n    <ProgressPrimitive.Root\n      data-slot=\"progress\"\n      className={cn(\n        \"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\",\n        className,\n      )}\n      {...props}\n    >\n      <ProgressPrimitive.Indicator\n        data-slot=\"progress-indicator\"\n        className=\"bg-primary h-full w-full flex-1 transition-all\"\n        style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n      />\n    </ProgressPrimitive.Root>\n  );\n}\n\nexport { Progress };\n"
  },
  {
    "path": "packages/shadcn/src/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CircleIcon } from \"lucide-react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction RadioGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n  return (\n    <RadioGroupPrimitive.Root\n      data-slot=\"radio-group\"\n      className={cn(\"grid gap-3\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction RadioGroupItem({\n  className,\n  ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n  return (\n    <RadioGroupPrimitive.Item\n      data-slot=\"radio-group-item\"\n      className={cn(\n        \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator\n        data-slot=\"radio-group-indicator\"\n        className=\"relative flex items-center justify-center\"\n      >\n        <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n}\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "packages/shadcn/src/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport { Select as SelectPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Select({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nfunction SelectGroup({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nfunction SelectValue({\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nfunction SelectTrigger({\n  className,\n  size = \"default\",\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      data-size={size}\n      className={cn(\n        \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 opacity-50\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectContent({\n  className,\n  children,\n  position = \"item-aligned\",\n  align = \"center\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        className={cn(\n          \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        position={position}\n        align={align}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        data-slot=\"select-item-indicator\"\n        className=\"absolute right-2 flex size-3.5 items-center justify-center\"\n      >\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nfunction SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Separator as SeparatorPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Separator({\n  className,\n  orientation = \"horizontal\",\n  decorative = true,\n  ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n  return (\n    <SeparatorPrimitive.Root\n      data-slot=\"separator\"\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Separator };\n"
  },
  {
    "path": "packages/shadcn/src/ui/skeleton.tsx",
    "content": "import { cn } from \"../lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"skeleton\"\n      className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "packages/shadcn/src/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Slider as SliderPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Slider({\n  className,\n  defaultValue,\n  value,\n  min = 0,\n  max = 100,\n  ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n  const _values = React.useMemo(\n    () =>\n      Array.isArray(value)\n        ? value\n        : Array.isArray(defaultValue)\n          ? defaultValue\n          : [min, max],\n    [value, defaultValue, min, max],\n  );\n\n  return (\n    <SliderPrimitive.Root\n      data-slot=\"slider\"\n      defaultValue={defaultValue}\n      value={value}\n      min={min}\n      max={max}\n      className={cn(\n        \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      {...props}\n    >\n      <SliderPrimitive.Track\n        data-slot=\"slider-track\"\n        className={cn(\n          \"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\",\n        )}\n      >\n        <SliderPrimitive.Range\n          data-slot=\"slider-range\"\n          className={cn(\n            \"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\",\n          )}\n        />\n      </SliderPrimitive.Track>\n      {Array.from({ length: _values.length }, (_, index) => (\n        <SliderPrimitive.Thumb\n          data-slot=\"slider-thumb\"\n          key={index}\n          className=\"border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\"\n        />\n      ))}\n    </SliderPrimitive.Root>\n  );\n}\n\nexport { Slider };\n"
  },
  {
    "path": "packages/shadcn/src/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Switch({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root> & {\n  size?: \"sm\" | \"default\";\n}) {\n  return (\n    <SwitchPrimitive.Root\n      data-slot=\"switch\"\n      data-size={size}\n      className={cn(\n        \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6\",\n        className,\n      )}\n      {...props}\n    >\n      <SwitchPrimitive.Thumb\n        data-slot=\"switch-thumb\"\n        className={cn(\n          \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\",\n        )}\n      />\n    </SwitchPrimitive.Root>\n  );\n}\n\nexport { Switch };\n"
  },
  {
    "path": "packages/shadcn/src/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n  return (\n    <div\n      data-slot=\"table-container\"\n      className=\"relative w-full overflow-x-auto\"\n    >\n      <table\n        data-slot=\"table\"\n        className={cn(\"w-full caption-bottom text-sm\", className)}\n        {...props}\n      />\n    </div>\n  );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\"[&_tr]:border-b\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n  return (\n    <tbody\n      data-slot=\"table-body\"\n      className={cn(\"[&_tr:last-child]:border-0\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n  return (\n    <tfoot\n      data-slot=\"table-footer\"\n      className={cn(\n        \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      className={cn(\n        \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n  return (\n    <th\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TableCaption({\n  className,\n  ...props\n}: React.ComponentProps<\"caption\">) {\n  return (\n    <caption\n      data-slot=\"table-caption\"\n      className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "packages/shadcn/src/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Tabs({\n  className,\n  orientation = \"horizontal\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"tabs\"\n      data-orientation={orientation}\n      orientation={orientation}\n      className={cn(\n        \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst tabsListVariants = cva(\n  \"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-muted\",\n        line: \"gap-1 bg-transparent\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nfunction TabsList({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n  VariantProps<typeof tabsListVariants>) {\n  return (\n    <TabsPrimitive.List\n      data-slot=\"tabs-list\"\n      data-variant={variant}\n      className={cn(tabsListVariants({ variant }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction TabsTrigger({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n  return (\n    <TabsPrimitive.Trigger\n      data-slot=\"tabs-trigger\"\n      className={cn(\n        \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n        \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground\",\n        \"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction TabsContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n  return (\n    <TabsPrimitive.Content\n      data-slot=\"tabs-content\"\n      className={cn(\"flex-1 outline-none\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };\n"
  },
  {
    "path": "packages/shadcn/src/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
  },
  {
    "path": "packages/shadcn/src/ui/toggle-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type VariantProps } from \"class-variance-authority\";\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\nimport { toggleVariants } from \"./toggle\";\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }\n>({\n  size: \"default\",\n  variant: \"default\",\n  spacing: 0,\n});\n\nfunction ToggleGroup({\n  className,\n  variant,\n  size,\n  spacing = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n  VariantProps<typeof toggleVariants> & {\n    spacing?: number;\n  }) {\n  return (\n    <ToggleGroupPrimitive.Root\n      data-slot=\"toggle-group\"\n      data-variant={variant}\n      data-size={size}\n      data-spacing={spacing}\n      style={{ \"--gap\": spacing } as React.CSSProperties}\n      className={cn(\n        \"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <ToggleGroupContext.Provider value={{ variant, size, spacing }}>\n        {children}\n      </ToggleGroupContext.Provider>\n    </ToggleGroupPrimitive.Root>\n  );\n}\n\nfunction ToggleGroupItem({\n  className,\n  children,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n  VariantProps<typeof toggleVariants>) {\n  const context = React.useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      data-slot=\"toggle-group-item\"\n      data-variant={context.variant || variant}\n      data-size={context.size || size}\n      data-spacing={context.spacing}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        \"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10\",\n        \"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n}\n\nexport { ToggleGroup, ToggleGroupItem };\n"
  },
  {
    "path": "packages/shadcn/src/ui/toggle.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-9 px-2 min-w-9\",\n        sm: \"h-8 px-1.5 min-w-8\",\n        lg: \"h-10 px-2.5 min-w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Toggle({\n  className,\n  variant,\n  size,\n  ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n  VariantProps<typeof toggleVariants>) {\n  return (\n    <TogglePrimitive.Root\n      data-slot=\"toggle\"\n      className={cn(toggleVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "packages/shadcn/src/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\n\nimport { cn } from \"../lib/utils\";\n\nfunction TooltipProvider({\n  delayDuration = 0,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      data-slot=\"tooltip-provider\"\n      delayDuration={delayDuration}\n      {...props}\n    />\n  );\n}\n\nfunction Tooltip({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n  return <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />;\n}\n\nfunction TooltipTrigger({\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 0,\n  children,\n  ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "packages/shadcn/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/shadcn/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/catalog.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"react\",\n    \"react-dom\",\n    \"@json-render/core\",\n    \"@json-render/react\",\n    \"zod\",\n  ],\n});\n"
  },
  {
    "path": "packages/solid/CHANGELOG.md",
    "content": "# @json-render/solid\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Minor Changes\n\n- 5b32de8: Add SolidJS and React Three Fiber renderers, plus strict JSON Schema mode for LLM structured outputs.\n\n  ### New:\n  - **`@json-render/solid`** -- SolidJS renderer. JSON becomes Solid components with reactive rendering, schema export, and full catalog support.\n  - **`@json-render/react-three-fiber`** -- React Three Fiber renderer. JSON becomes 3D scenes with 19 built-in components for meshes, lights, models, environments, text, cameras, and controls.\n\n  ### Improved:\n  - **`@json-render/core`** -- `jsonSchema({ strict: true })` produces a JSON Schema subset compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic). Ensures `additionalProperties: false` on every object and all properties listed in `required`.\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n"
  },
  {
    "path": "packages/solid/README.md",
    "content": "# @json-render/solid\n\nSolidJS renderer for json-render. Turn JSON specs into Solid components with data binding, visibility, actions, validation, and streaming.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/solid zod\n```\n\nPeer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.\n\n## Quick Start\n\n### 1. Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/solid/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"A card container\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n      }),\n      description: \"A clickable button\",\n    },\n    Input: {\n      props: z.object({\n        value: z.union([z.string(), z.record(z.unknown())]).nullable(),\n        label: z.string(),\n        placeholder: z.string().nullable(),\n      }),\n      description: \"Text input with optional state binding\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit the form\" },\n    cancel: { description: \"Cancel and close\" },\n  },\n});\n```\n\n### 2. Define Component Implementations\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions.\n\n```tsx\nimport { defineRegistry, useBoundProp } from \"@json-render/solid\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: (renderProps) => (\n      <div class=\"card\">\n        <h3>{renderProps.element.props.title as string}</h3>\n        {renderProps.children}\n      </div>\n    ),\n    Button: (renderProps) => (\n      <button onClick={() => renderProps.emit(\"press\")}>\n        {renderProps.element.props.label as string}\n      </button>\n    ),\n    Input: (renderProps) => {\n      const [value, setValue] = useBoundProp(\n        renderProps.element.props.value,\n        renderProps.bindings?.value,\n      );\n      return (\n        <label>\n          {renderProps.element.props.label as string}\n          <input\n            value={String(value() ?? \"\")}\n            placeholder={String(renderProps.element.props.placeholder ?? \"\")}\n            onInput={(e) => setValue(e.currentTarget.value)}\n          />\n        </label>\n      );\n    },\n  },\n  actions: {\n    submit: async () => {},\n    cancel: async () => {},\n  },\n});\n```\n\n### 3. Render Specs\n\n```tsx\nimport { Renderer, StateProvider, ActionProvider } from \"@json-render/solid\";\nimport { registry } from \"./registry\";\n\nexport function App(props: { spec: any }) {\n  return (\n    <StateProvider initialState={{ form: { name: \"\" } }}>\n      <ActionProvider handlers={{ submit: () => console.log(\"submit\") }}>\n        <Renderer spec={props.spec} registry={registry} />\n      </ActionProvider>\n    </StateProvider>\n  );\n}\n```\n\n## Spec Format\n\n`@json-render/solid` uses the same flat element map format as React/Vue:\n\n```typescript\ninterface Spec {\n  root: string;\n  elements: Record<string, UIElement>;\n  state?: Record<string, unknown>;\n}\n\ninterface UIElement {\n  type: string;\n  props: Record<string, unknown>;\n  children?: string[];\n  visible?: VisibilityCondition;\n  watch?: Record<string, ActionBinding | ActionBinding[]>;\n}\n```\n\n## Providers\n\n| Provider              | Purpose                                              |\n| --------------------- | ---------------------------------------------------- |\n| `StateProvider`       | State model and JSON Pointer read/write APIs         |\n| `ActionProvider`      | Action dispatch, loading tracking, confirmation flow |\n| `VisibilityProvider`  | Visibility condition evaluation from current state   |\n| `ValidationProvider`  | Field-level and full-form validation                 |\n| `RepeatScopeProvider` | Repeat context (`$item`, `$index`, `$bindItem`)      |\n| `JSONUIProvider`      | Combined provider wiring for renderer trees          |\n\n### External Store (Controlled Mode)\n\nPass a `StateStore` to `StateProvider`, `JSONUIProvider`, or the component returned by `createRenderer`.\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n```tsx\nimport { createStateStore, StateProvider } from \"@json-render/solid\";\n\nconst store = createStateStore({ count: 0 });\n\n<StateProvider store={store}>{/* ... */}</StateProvider>;\n```\n\n## Hooks\n\n| Hook                                          | Purpose                                               |\n| --------------------------------------------- | ----------------------------------------------------- |\n| `useStateStore()`                             | Access `state`, `get`, `set`, `update`, `getSnapshot` |\n| `useStateValue(path)`                         | Read a value by JSON Pointer path via accessor        |\n| `useStateBinding(path)`                       | Legacy two-way binding helper returning an accessor   |\n| `useVisibility()` / `useIsVisible()`          | Visibility context and checks                         |\n| `useActions()` / `useAction()`                | Action context and single-action helper               |\n| `useValidation()` / `useOptionalValidation()` | Validation context (throwing/non-throwing)            |\n| `useFieldValidation(path, config)`            | Field state accessors plus validate/touch/clear       |\n| `useBoundProp(value, binding)`                | Fine-grained two-way binding helper                   |\n| `useUIStream(options)`                        | Stream UI specs from an endpoint                      |\n| `useChatUI(options)`                          | Chat-style spec generation hook                       |\n\n## Built-in Actions\n\nThese actions are available in the Solid schema and handled by `ActionProvider`:\n\n- `setState`\n- `pushState`\n- `removeState`\n- `validateForm`\n\n`setState`/`pushState`/`removeState` mutate the state model. `validateForm` validates registered fields and writes `{ valid, errors }` to state (`/formValidation` by default).\n\n## Events and Action Binding\n\nComponents can use either `emit(\"event\")` or `on(\"event\")`.\n\n- `emit` fires named event bindings directly.\n- `on` returns an `EventHandle` with `emit`, `bound`, and `shouldPreventDefault`.\n\nThis mirrors the React package API while preserving Solid's fine-grained reactivity.\n\n## Streaming\n\n`useUIStream` and `useChatUI` support JSON patch streaming and mixed text/spec data parts.\n\n```tsx\nimport { useUIStream } from \"@json-render/solid\";\n\nconst stream = useUIStream({ api: \"/api/generate\" });\nawait stream.send(\"Build me a dashboard\");\n```\n\n## Key Exports\n\n| Export             | Purpose                                                                    |\n| ------------------ | -------------------------------------------------------------------------- |\n| `defineRegistry`   | Create catalog-aware component and action registry helpers                 |\n| `Renderer`         | Render a `Spec` with a component registry                                  |\n| `createRenderer`   | Build an app-level renderer with provider wiring                           |\n| `JSONUIProvider`   | Combined provider tree (`state` + `visibility` + `validation` + `actions`) |\n| `schema`           | Solid element schema with built-in actions                                 |\n| `createStateStore` | Framework-agnostic in-memory `StateStore`                                  |\n\n### Types\n\n| Export                      | Purpose                                                  |\n| --------------------------- | -------------------------------------------------------- |\n| `ComponentContext`          | Catalog-aware component context type                     |\n| `BaseComponentProps`        | Catalog-agnostic component props type                    |\n| `EventHandle`               | Event metadata (`emit`, `bound`, `shouldPreventDefault`) |\n| `StateStore`                | Controlled state backend interface                       |\n| `StateModel`                | Renderer state model type                                |\n| `SolidSchema` / `SolidSpec` | Solid schema/spec types                                  |\n\n## Differences from `@json-render/react`\n\nMost APIs are intentionally aligned, but there are runtime behavior differences due to Solid:\n\n- Solid components run once, then update via signals.\n- Keep changing reads inside JSX expressions, `createMemo`, or `createEffect`.\n- Avoid props destructuring in component signatures when values should remain reactive.\n- Hooks that read changing state return accessors; call them inside JSX or effects.\n\n## Documentation\n\nFull docs: [json-render.dev/docs/api/solid](https://json-render.dev/docs/api/solid)\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/solid/package.json",
    "content": "{\n  \"name\": \"@json-render/solid\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"SolidJS renderer for @json-render/core. JSON becomes Solid components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"solid\",\n    \"solidjs\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/solid\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./schema\": {\n      \"types\": \"./dist/schema.d.ts\",\n      \"import\": \"./dist/schema.mjs\",\n      \"require\": \"./dist/schema.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"esbuild-plugin-solid\": \"^0.6.0\",\n    \"solid-js\": \"^1.9.0\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"peerDependencies\": {\n    \"solid-js\": \"^1.9.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/solid/src/catalog-types.ts",
    "content": "import type { JSX } from \"solid-js\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\n/**\n * State setter function for updating application state\n */\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\n/**\n * Handle returned by the `on()` function for a specific event.\n * Provides metadata about the event binding and a method to fire it.\n *\n * @example\n * ```ts\n * const press = on(\"press\");\n * if (press.shouldPreventDefault) e.preventDefault();\n * press.emit();\n * ```\n */\nexport interface EventHandle {\n  /** Fire the event (resolve action bindings) */\n  emit: () => void;\n  /** Whether any binding requested preventDefault */\n  shouldPreventDefault: boolean;\n  /** Whether any handler is bound to this event */\n  bound: boolean;\n}\n\n/**\n * Catalog-agnostic base type for component render function arguments.\n * Use this when building reusable component libraries that are not tied\n * to a specific catalog.\n *\n * @example\n * ```ts\n * const Card = (ctx: BaseComponentProps<{ title?: string }>) => (\n *   <div>{ctx.props.title}{ctx.children}</div>\n * );\n * ```\n */\nexport interface BaseComponentProps<P = Record<string, unknown>> {\n  props: P;\n  children?: JSX.Element;\n  /** Simple event emitter (shorthand). Fires the event and returns void. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\n/**\n * Context passed to component render functions\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => {\n *   return <button onClick={() => ctx.emit(\"press\")}>{ctx.props.label}</button>\n * }\n */\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> extends BaseComponentProps<InferComponentProps<C, K>> {}\n\n/**\n * Component render function type for Solid\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => (\n *   <button onClick={() => ctx.emit(\"press\")}>{ctx.props.label}</button>\n * );\n */\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => JSX.Element;\n\n/**\n * Registry of all component render functions for a catalog\n * @example\n * const components: Components<typeof myCatalog> = {\n *   Button: (ctx) => <button>{ctx.props.label}</button>,\n *   Input: (ctx) => <input placeholder={ctx.props.placeholder} />,\n * };\n */\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n\n// =============================================================================\n// Action Types\n// =============================================================================\n\n/**\n * Action handler function type\n * @example\n * const viewCustomers: ActionFn<typeof catalog, 'viewCustomers'> = async (params, setState) => {\n *   const data = await fetch('/api/customers');\n *   setState(prev => ({ ...prev, customers: data }));\n * };\n */\nexport type ActionFn<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = (\n  params: InferActionParams<C, K> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Registry of all action handlers for a catalog\n * @example\n * const actions: Actions<typeof myCatalog> = {\n *   viewCustomers: async (params, setState) => { ... },\n *   createCustomer: async (params, setState) => { ... },\n * };\n */\nexport type Actions<C extends Catalog> = {\n  [K in keyof InferCatalogActions<C>]: ActionFn<C, K>;\n};\n\n/**\n * True when the catalog declares at least one action, false otherwise.\n * Used by defineRegistry to conditionally require the `actions` field.\n */\nexport type CatalogHasActions<C extends Catalog> = [\n  InferCatalogActions<C>,\n] extends [never]\n  ? false\n  : [keyof InferCatalogActions<C>] extends [never]\n    ? false\n    : true;\n"
  },
  {
    "path": "packages/solid/src/chained-actions.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@solidjs/testing-library\";\nimport type { Spec } from \"@json-render/core\";\nimport {\n  JSONUIProvider,\n  Renderer,\n  type ComponentRenderProps,\n} from \"./renderer\";\nimport { useStateStore } from \"./contexts/state\";\n\n// DO NOT destructure props — Solid requires proxy access\nfunction Button(props: ComponentRenderProps<{ label: string }>) {\n  return (\n    <button data-testid=\"btn\" onClick={() => props.emit(\"press\")}>\n      {props.element.props.label}\n    </button>\n  );\n}\n\nfunction Text(props: ComponentRenderProps<{ text: unknown }>) {\n  const display = () => {\n    const v = props.element.props.text;\n    return typeof v === \"string\" ? v : JSON.stringify(v);\n  };\n  return <span data-testid={`text-${props.element.type}`}>{display()}</span>;\n}\n\nlet probeGetSnapshot: (() => Record<string, unknown>) | undefined;\n\nfunction StateProbe() {\n  const ctx = useStateStore();\n  probeGetSnapshot = ctx.getSnapshot;\n  return <div data-testid=\"state-probe\" />;\n}\n\nconst registry = {\n  Button,\n  Text,\n};\n\nfunction getProbeState(): Record<string, unknown> {\n  return probeGetSnapshot!();\n}\n\ndescribe(\"chained actions: live $state resolution (#141)\", () => {\n  it(\"setState after pushState sees the post-push value via $state\", async () => {\n    probeGetSnapshot = undefined;\n    const spec: Spec = {\n      state: { items: [\"initial\"], observed: \"not yet set\" },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Add Item\" },\n          on: {\n            press: [\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"new-item\" },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/observed\",\n                  value: { $state: \"/items\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(() => <App />);\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getProbeState();\n      expect(state.items).toEqual([\"initial\", \"new-item\"]);\n      expect(state.observed).toEqual([\"initial\", \"new-item\"]);\n    });\n  });\n\n  it(\"multiple pushState + setState chain resolves correctly\", async () => {\n    probeGetSnapshot = undefined;\n    const spec: Spec = {\n      state: { items: [], snapshot: null },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Go\" },\n          on: {\n            press: [\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"a\" },\n              },\n              {\n                action: \"pushState\",\n                params: { statePath: \"/items\", value: \"b\" },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/snapshot\",\n                  value: { $state: \"/items\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(() => <App />);\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getProbeState();\n      expect(state.items).toEqual([\"a\", \"b\"]);\n      expect(state.snapshot).toEqual([\"a\", \"b\"]);\n    });\n  });\n\n  it(\"setState reading a path mutated by an earlier setState sees fresh value\", async () => {\n    probeGetSnapshot = undefined;\n    const spec: Spec = {\n      state: { counter: 0, counterCopy: -1 },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Button\",\n          props: { label: \"Go\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/counter\", value: 42 },\n              },\n              {\n                action: \"setState\",\n                params: {\n                  statePath: \"/counterCopy\",\n                  value: { $state: \"/counter\" },\n                },\n              },\n            ],\n          },\n        },\n      },\n    };\n\n    function App() {\n      return (\n        <JSONUIProvider registry={registry} initialState={spec.state}>\n          <Renderer spec={spec} registry={registry} />\n          <StateProbe />\n        </JSONUIProvider>\n      );\n    }\n\n    render(() => <App />);\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getProbeState();\n      expect(state.counter).toBe(42);\n      expect(state.counterCopy).toBe(42);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/contexts/actions.test.tsx",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { render } from \"@solidjs/testing-library\";\nimport type { JSX } from \"solid-js\";\nimport { StateProvider, useStateStore } from \"./state\";\nimport {\n  ValidationProvider,\n  useValidation,\n  useFieldValidation,\n} from \"./validation\";\nimport { ActionProvider, useActions, useAction } from \"./actions\";\n\n/**\n * Build the full provider tree inline so that Solid's eager JSX evaluation\n * always runs ActionProvider/ValidationProvider INSIDE StateProvider's scope.\n *\n * IMPORTANT: Do NOT pre-build JSX fragments as variables — Solid evaluates\n * JSX eagerly, so `<ActionProvider>` would call `useStateStore()` before the\n * StateProvider context is available.\n */\nfunction withProviders<T>(\n  hook: () => T,\n  options: {\n    handlers?: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void> | void\n    >;\n    initialState?: Record<string, unknown>;\n    withValidation?: boolean;\n  } = {},\n): T {\n  let result!: T;\n  function TestComponent(): JSX.Element {\n    result = hook();\n    return (<div />) as JSX.Element;\n  }\n\n  if (options.withValidation) {\n    render(() => (\n      <StateProvider initialState={options.initialState ?? {}}>\n        <ValidationProvider>\n          <ActionProvider handlers={options.handlers}>\n            <TestComponent />\n          </ActionProvider>\n        </ValidationProvider>\n      </StateProvider>\n    ));\n  } else {\n    render(() => (\n      <StateProvider initialState={options.initialState ?? {}}>\n        <ActionProvider handlers={options.handlers}>\n          <TestComponent />\n        </ActionProvider>\n      </StateProvider>\n    ));\n  }\n  return result;\n}\n\nfunction withFullProviders(options: {\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<void> | void\n  >;\n  initialState?: Record<string, unknown>;\n  withValidation?: boolean;\n}): {\n  stateCtx: ReturnType<typeof useStateStore>;\n  actionsCtx: ReturnType<typeof useActions>;\n  validationCtx?: ReturnType<typeof useValidation>;\n} {\n  let stateCtx!: ReturnType<typeof useStateStore>;\n  let actionsCtx!: ReturnType<typeof useActions>;\n  let validationCtx: ReturnType<typeof useValidation> | undefined;\n  function TestComponent(): JSX.Element {\n    stateCtx = useStateStore();\n    actionsCtx = useActions();\n    if (options.withValidation) {\n      validationCtx = useValidation();\n    }\n    return (<div />) as JSX.Element;\n  }\n\n  if (options.withValidation) {\n    render(() => (\n      <StateProvider initialState={options.initialState ?? {}}>\n        <ValidationProvider>\n          <ActionProvider handlers={options.handlers}>\n            <TestComponent />\n          </ActionProvider>\n        </ValidationProvider>\n      </StateProvider>\n    ));\n  } else {\n    render(() => (\n      <StateProvider initialState={options.initialState ?? {}}>\n        <ActionProvider handlers={options.handlers}>\n          <TestComponent />\n        </ActionProvider>\n      </StateProvider>\n    ));\n  }\n  return { stateCtx, actionsCtx, validationCtx };\n}\n\ndescribe(\"ActionProvider — provide/inject\", () => {\n  it(\"useActions() throws outside a provider\", () => {\n    expect(() => useActions()).toThrow(\n      \"useActions must be used within an ActionProvider\",\n    );\n  });\n});\n\ndescribe(\"ActionProvider — built-in setState\", () => {\n  it(\"executes setState and updates state\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: { count: 0 },\n    });\n\n    await actionsCtx.execute({\n      action: \"setState\",\n      params: { statePath: \"/count\", value: 5 },\n    });\n\n    expect(stateCtx.get(\"/count\")).toBe(5);\n  });\n});\n\ndescribe(\"ActionProvider — built-in pushState\", () => {\n  it(\"appends to existing array\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: { items: [\"a\", \"b\"] },\n    });\n\n    await actionsCtx.execute({\n      action: \"pushState\",\n      params: { statePath: \"/items\", value: \"c\" },\n    });\n\n    expect(stateCtx.get(\"/items\")).toEqual([\"a\", \"b\", \"c\"]);\n  });\n\n  it(\"creates array if path does not exist\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: {},\n    });\n\n    await actionsCtx.execute({\n      action: \"pushState\",\n      params: { statePath: \"/newList\", value: \"first\" },\n    });\n\n    expect(stateCtx.get(\"/newList\")).toEqual([\"first\"]);\n  });\n});\n\ndescribe(\"ActionProvider — built-in removeState\", () => {\n  it(\"removes item by index\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: { items: [\"a\", \"b\", \"c\"] },\n    });\n\n    await actionsCtx.execute({\n      action: \"removeState\",\n      params: { statePath: \"/items\", index: 1 },\n    });\n\n    expect(stateCtx.get(\"/items\")).toEqual([\"a\", \"c\"]);\n  });\n});\n\ndescribe(\"ActionProvider — built-in push/pop navigation\", () => {\n  it(\"push sets /currentScreen and /navStack\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: { currentScreen: \"home\" },\n    });\n\n    await actionsCtx.execute({\n      action: \"push\",\n      params: { screen: \"settings\" },\n    });\n\n    expect(stateCtx.get(\"/currentScreen\")).toBe(\"settings\");\n    expect(stateCtx.get(\"/navStack\")).toEqual([\"home\"]);\n  });\n\n  it(\"pop restores previous screen from /navStack\", async () => {\n    const { stateCtx, actionsCtx } = withFullProviders({\n      initialState: { currentScreen: \"settings\", navStack: [\"home\"] },\n    });\n\n    await actionsCtx.execute({ action: \"pop\" });\n\n    expect(stateCtx.get(\"/currentScreen\")).toBe(\"home\");\n    expect(stateCtx.get(\"/navStack\")).toEqual([]);\n  });\n});\n\ndescribe(\"ActionProvider — custom handlers\", () => {\n  it(\"executes custom handler with params\", async () => {\n    const customHandler = vi.fn().mockResolvedValue(undefined);\n    const { actionsCtx } = withFullProviders({\n      handlers: { myAction: customHandler },\n    });\n\n    await actionsCtx.execute({\n      action: \"myAction\",\n      params: { foo: \"bar\" },\n    });\n\n    expect(customHandler).toHaveBeenCalledWith({ foo: \"bar\" });\n  });\n\n  it(\"console.warn for unknown action\", async () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const { actionsCtx } = withFullProviders({});\n\n    await actionsCtx.execute({ action: \"unknownAction\" });\n\n    expect(warnSpy).toHaveBeenCalledWith(\n      expect.stringContaining(\"unknownAction\"),\n    );\n    warnSpy.mockRestore();\n  });\n\n  it(\"tracks loading state during async action execution\", async () => {\n    let resolveHandler!: () => void;\n    const slowHandler = vi.fn(\n      () =>\n        new Promise<void>((resolve) => {\n          resolveHandler = resolve;\n        }),\n    );\n\n    let executeFn!: ReturnType<typeof useActions>[\"execute\"];\n\n    const { findByTestId } = render(() => {\n      function Inner(): JSX.Element {\n        const actions = useActions();\n        executeFn = actions.execute;\n        return (\n          <span data-testid=\"loading\">\n            {actions.loadingActions.has(\"slowAction\") ? \"true\" : \"false\"}\n          </span>\n        );\n      }\n      return (\n        <StateProvider initialState={{}}>\n          <ActionProvider handlers={{ slowAction: slowHandler }}>\n            <Inner />\n          </ActionProvider>\n        </StateProvider>\n      );\n    });\n\n    // Before execution — not loading\n    const el = await findByTestId(\"loading\");\n    expect(el.textContent).toBe(\"false\");\n\n    const executePromise = executeFn({ action: \"slowAction\" });\n\n    // During execution — loading (need to wait for Solid's reactivity to flush)\n    await vi.waitFor(() => {\n      expect(el.textContent).toBe(\"true\");\n    });\n\n    resolveHandler();\n    await executePromise;\n\n    // After execution — no longer loading\n    await vi.waitFor(() => {\n      expect(el.textContent).toBe(\"false\");\n    });\n  });\n\n  it(\"registerHandler allows dynamic handler registration\", async () => {\n    const dynamicHandler = vi.fn().mockResolvedValue(undefined);\n    const { actionsCtx } = withFullProviders({});\n\n    actionsCtx.registerHandler(\"dynamicAction\", dynamicHandler);\n    await actionsCtx.execute({\n      action: \"dynamicAction\",\n      params: { x: 1 },\n    });\n\n    expect(dynamicHandler).toHaveBeenCalledWith({ x: 1 });\n  });\n\n  it(\"handler receives resolved params object\", async () => {\n    const handler = vi.fn().mockResolvedValue(undefined);\n    const { actionsCtx } = withFullProviders({\n      handlers: { myAction: handler },\n    });\n\n    await actionsCtx.execute({\n      action: \"myAction\",\n      params: { x: 1, y: \"hello\" },\n    });\n\n    expect(handler).toHaveBeenCalledWith({ x: 1, y: \"hello\" });\n  });\n});\n\ndescribe(\"ActionProvider — validateForm\", () => {\n  it(\"writes { valid, errors } to state\", async () => {\n    const { stateCtx, actionsCtx, validationCtx } = withFullProviders({\n      initialState: {},\n      withValidation: true,\n    });\n\n    validationCtx!.registerField(\"/form/email\", {\n      checks: [{ type: \"required\", message: \"Required\" }],\n    });\n\n    await actionsCtx.execute({ action: \"validateForm\" });\n\n    expect(stateCtx.get(\"/formValidation\")).toEqual({\n      valid: false,\n      errors: { \"/form/email\": [\"Required\"] },\n    });\n  });\n\n  it(\"warns without ValidationProvider\", async () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const { actionsCtx } = withFullProviders({\n      withValidation: false,\n    });\n\n    await actionsCtx.execute({ action: \"validateForm\" });\n\n    expect(warnSpy).toHaveBeenCalledWith(\n      expect.stringContaining(\"validateForm action was dispatched\"),\n    );\n    warnSpy.mockRestore();\n  });\n});\n\ndescribe(\"useAction\", () => {\n  it(\"returns { execute, isLoading: false } before execution\", () => {\n    const result = withProviders(() => useAction({ action: \"myAction\" }), {\n      handlers: { myAction: vi.fn() },\n    });\n\n    expect(typeof result.execute).toBe(\"function\");\n    expect(result.isLoading).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/contexts/actions.tsx",
    "content": "import {\n  createContext,\n  useContext,\n  createSignal,\n  type ParentProps,\n  type JSX,\n} from \"solid-js\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\nimport { useOptionalValidation } from \"./validation\";\n\nlet idCounter = 0;\nfunction generateUniqueId(): string {\n  idCounter += 1;\n  return `${Date.now()}-${idCounter}`;\n}\n\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\nexport interface PendingConfirmation {\n  action: ResolvedAction;\n  handler: ActionHandler;\n  resolve: () => void;\n  reject: () => void;\n}\n\nexport interface ActionContextValue {\n  handlers: Record<string, ActionHandler>;\n  loadingActions: Set<string>;\n  pendingConfirmation: PendingConfirmation | null;\n  execute: (binding: ActionBinding) => Promise<void>;\n  confirm: () => void;\n  cancel: () => void;\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ActionContext = createContext<ActionContextValue | null>(null);\n\nexport interface ActionProviderProps {\n  handlers?: Record<string, ActionHandler>;\n  navigate?: (path: string) => void;\n}\n\nexport function ActionProvider(props: ParentProps<ActionProviderProps>) {\n  const { get, set, getSnapshot } = useStateStore();\n  const validation = useOptionalValidation();\n\n  const [handlers, setHandlers] = createSignal<Record<string, ActionHandler>>(\n    props.handlers ?? {},\n  );\n  const [loadingActions, setLoadingActions] = createSignal<Set<string>>(\n    new Set(),\n  );\n  const [pendingConfirmation, setPendingConfirmation] =\n    createSignal<PendingConfirmation | null>(null);\n\n  const registerHandler = (name: string, handler: ActionHandler) => {\n    setHandlers((prev) => ({ ...prev, [name]: handler }));\n  };\n\n  const execute = async (binding: ActionBinding) => {\n    const resolved = resolveAction(binding, getSnapshot());\n\n    if (resolved.action === \"setState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const value = resolved.params.value;\n      if (statePath) {\n        set(statePath, value);\n      }\n      return;\n    }\n\n    if (resolved.action === \"pushState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const rawValue = resolved.params.value;\n      if (statePath) {\n        const resolvedValue = deepResolveValue(rawValue, get);\n        const arr = (get(statePath) as unknown[] | undefined) ?? [];\n        set(statePath, [...arr, resolvedValue]);\n        const clearStatePath = resolved.params.clearStatePath as\n          | string\n          | undefined;\n        if (clearStatePath) {\n          set(clearStatePath, \"\");\n        }\n      }\n      return;\n    }\n\n    if (resolved.action === \"removeState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const index = resolved.params.index as number;\n      if (statePath !== undefined && index !== undefined) {\n        const arr = (get(statePath) as unknown[] | undefined) ?? [];\n        set(\n          statePath,\n          arr.filter((_, i) => i !== index),\n        );\n      }\n      return;\n    }\n\n    if (resolved.action === \"push\" && resolved.params) {\n      const screen = resolved.params.screen as string;\n      if (screen) {\n        const currentScreen = get(\"/currentScreen\") as string | undefined;\n        const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n        if (currentScreen) {\n          set(\"/navStack\", [...navStack, currentScreen]);\n        } else {\n          set(\"/navStack\", [...navStack, \"\"]);\n        }\n        set(\"/currentScreen\", screen);\n      }\n      return;\n    }\n\n    if (resolved.action === \"pop\") {\n      const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n      if (navStack.length > 0) {\n        const previousScreen = navStack[navStack.length - 1];\n        set(\"/navStack\", navStack.slice(0, -1));\n        if (previousScreen) {\n          set(\"/currentScreen\", previousScreen);\n        } else {\n          set(\"/currentScreen\", undefined);\n        }\n      }\n      return;\n    }\n\n    if (resolved.action === \"validateForm\") {\n      const validateAll = validation?.validateAll;\n      if (!validateAll) {\n        console.warn(\n          \"validateForm action was dispatched but no ValidationProvider is connected. \" +\n            \"Ensure ValidationProvider is rendered inside the provider tree.\",\n        );\n        return;\n      }\n      const valid = validateAll();\n      const errors: Record<string, string[]> = {};\n      for (const [path, fs] of Object.entries(validation.fieldStates)) {\n        if (fs.result && !fs.result.valid) {\n          errors[path] = fs.result.errors;\n        }\n      }\n      const statePath =\n        (resolved.params?.statePath as string) || \"/formValidation\";\n      set(statePath, { valid, errors });\n      return;\n    }\n\n    const handler = handlers()[resolved.action];\n\n    if (!handler) {\n      console.warn(`No handler registered for action: ${resolved.action}`);\n      return;\n    }\n\n    if (resolved.confirm) {\n      return new Promise<void>((resolve, reject) => {\n        setPendingConfirmation({\n          action: resolved,\n          handler,\n          resolve: () => {\n            setPendingConfirmation(null);\n            resolve();\n          },\n          reject: () => {\n            setPendingConfirmation(null);\n            reject(new Error(\"Action cancelled\"));\n          },\n        });\n      }).then(async () => {\n        setLoadingActions((prev) => new Set(prev).add(resolved.action));\n        try {\n          await executeAction({\n            action: resolved,\n            handler,\n            setState: set,\n            navigate: props.navigate,\n            executeAction: async (name: string) => {\n              const subBinding: ActionBinding = { action: name };\n              await execute(subBinding);\n            },\n          });\n        } finally {\n          setLoadingActions((prev) => {\n            const next = new Set(prev);\n            next.delete(resolved.action);\n            return next;\n          });\n        }\n      });\n    }\n\n    setLoadingActions((prev) => new Set(prev).add(resolved.action));\n    try {\n      await executeAction({\n        action: resolved,\n        handler,\n        setState: set,\n        navigate: props.navigate,\n        executeAction: async (name: string) => {\n          const subBinding: ActionBinding = { action: name };\n          await execute(subBinding);\n        },\n      });\n    } finally {\n      setLoadingActions((prev) => {\n        const next = new Set(prev);\n        next.delete(resolved.action);\n        return next;\n      });\n    }\n  };\n\n  const confirm = () => {\n    pendingConfirmation()?.resolve();\n  };\n\n  const cancel = () => {\n    pendingConfirmation()?.reject();\n  };\n\n  const ctx: ActionContextValue = {\n    get handlers() {\n      return handlers();\n    },\n    get loadingActions() {\n      return loadingActions();\n    },\n    get pendingConfirmation() {\n      return pendingConfirmation();\n    },\n    execute,\n    confirm,\n    cancel,\n    registerHandler,\n  };\n\n  return (\n    <ActionContext.Provider value={ctx}>\n      {props.children}\n    </ActionContext.Provider>\n  );\n}\n\nexport function useActions(): ActionContextValue {\n  const ctx = useContext(ActionContext);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: boolean;\n} {\n  const actions = useActions();\n  return {\n    execute: () => actions.execute(binding),\n    get isLoading() {\n      return actions.loadingActions.has(binding.action);\n    },\n  };\n}\n\nexport interface ConfirmDialogProps {\n  confirm: ActionConfirm;\n  onConfirm: () => void;\n  onCancel: () => void;\n}\n\nexport function ConfirmDialog(props: ConfirmDialogProps): JSX.Element {\n  const isDanger = () => props.confirm.variant === \"danger\";\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        inset: \"0\",\n        \"background-color\": \"rgba(0, 0, 0, 0.5)\",\n        display: \"flex\",\n        \"align-items\": \"center\",\n        \"justify-content\": \"center\",\n        \"z-index\": 50,\n      }}\n      onClick={props.onCancel}\n    >\n      <div\n        style={{\n          \"background-color\": \"white\",\n          \"border-radius\": \"8px\",\n          padding: \"24px\",\n          \"max-width\": \"400px\",\n          width: \"100%\",\n          \"box-shadow\": \"0 20px 25px -5px rgba(0, 0, 0, 0.1)\",\n        }}\n        onClick={(e) => e.stopPropagation()}\n      >\n        <h3\n          style={{\n            margin: \"0 0 8px 0\",\n            \"font-size\": \"18px\",\n            \"font-weight\": \"600\",\n          }}\n        >\n          {props.confirm.title}\n        </h3>\n        <p\n          style={{\n            margin: \"0 0 24px 0\",\n            color: \"#6b7280\",\n          }}\n        >\n          {props.confirm.message}\n        </p>\n        <div\n          style={{\n            display: \"flex\",\n            gap: \"12px\",\n            \"justify-content\": \"flex-end\",\n          }}\n        >\n          <button\n            onClick={props.onCancel}\n            style={{\n              padding: \"8px 16px\",\n              \"border-radius\": \"6px\",\n              border: \"1px solid #d1d5db\",\n              \"background-color\": \"white\",\n              cursor: \"pointer\",\n            }}\n          >\n            {props.confirm.cancelLabel ?? \"Cancel\"}\n          </button>\n          <button\n            onClick={props.onConfirm}\n            style={{\n              padding: \"8px 16px\",\n              \"border-radius\": \"6px\",\n              border: \"none\",\n              \"background-color\": isDanger() ? \"#dc2626\" : \"#3b82f6\",\n              color: \"white\",\n              cursor: \"pointer\",\n            }}\n          >\n            {props.confirm.confirmLabel ?? \"Confirm\"}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/solid/src/contexts/repeat-scope.tsx",
    "content": "import { createContext, useContext, type ParentProps } from \"solid-js\";\n\nexport interface RepeatScopeValue {\n  item: unknown;\n  index: number;\n  basePath: string;\n}\n\nconst RepeatScopeContext = createContext<RepeatScopeValue | null>(null);\n\nexport function RepeatScopeProvider(props: ParentProps<RepeatScopeValue>) {\n  return (\n    <RepeatScopeContext.Provider\n      value={{ item: props.item, index: props.index, basePath: props.basePath }}\n    >\n      {props.children}\n    </RepeatScopeContext.Provider>\n  );\n}\n\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return useContext(RepeatScopeContext);\n}\n"
  },
  {
    "path": "packages/solid/src/contexts/state.test.tsx",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { render } from \"@solidjs/testing-library\";\nimport { createEffect, createSignal, type Accessor, type JSX } from \"solid-js\";\nimport { createStateStore } from \"@json-render/core\";\nimport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n} from \"./state\";\n\nfunction renderWithState<T>(\n  hook: () => T,\n  initialState: Record<string, unknown> = {},\n  extraProps?: { store?: any; onStateChange?: any },\n): T {\n  let result!: T;\n  function TestComponent(): JSX.Element {\n    result = hook();\n    return (<div data-testid=\"test\" />) as unknown as JSX.Element;\n  }\n  render(() => (\n    <StateProvider initialState={initialState} {...(extraProps ?? {})}>\n      <TestComponent />\n    </StateProvider>\n  ));\n  return result;\n}\n\ndescribe(\"StateProvider + hooks\", () => {\n  it(\"useStateStore() throws outside provider\", () => {\n    expect(() => useStateStore()).toThrow(\n      \"useStateStore must be used within a StateProvider\",\n    );\n  });\n\n  it(\"StateProvider + useStateStore round-trip (get + set)\", () => {\n    const ctx = renderWithState(() => useStateStore(), { count: 0 });\n\n    expect(ctx.get(\"/count\")).toBe(0);\n    ctx.set(\"/count\", 42);\n    expect(ctx.get(\"/count\")).toBe(42);\n  });\n\n  it(\"useStateValue reads from state\", () => {\n    const value = renderWithState(() => useStateValue(\"/name\"), {\n      name: \"Alice\",\n    });\n\n    expect(value()).toBe(\"Alice\");\n  });\n\n  it(\"useStateBinding returns value and setter\", () => {\n    const [value, setValue] = renderWithState(() => useStateBinding(\"/x\"), {\n      x: 1,\n    });\n\n    expect(value()).toBe(1);\n    expect(typeof setValue).toBe(\"function\");\n  });\n\n  it(\"useStateValue stays reactive after state updates\", () => {\n    const observed: Array<string | undefined> = [];\n\n    function TestComponent(): JSX.Element {\n      const value = useStateValue<string>(\"/name\");\n      createEffect(() => {\n        observed.push(value());\n      });\n      const store = useStateStore();\n      return (\n        <button\n          data-testid=\"update\"\n          onClick={() => store.set(\"/name\", \"Bob\")}\n        />\n      ) as unknown as JSX.Element;\n    }\n\n    const { getByTestId } = render(() => (\n      <StateProvider initialState={{ name: \"Alice\" }}>\n        <TestComponent />\n      </StateProvider>\n    ));\n\n    expect(observed).toEqual([\"Alice\"]);\n    getByTestId(\"update\").click();\n    expect(observed).toEqual([\"Alice\", \"Bob\"]);\n  });\n\n  it(\"useStateBinding value accessor stays reactive after state updates\", () => {\n    const observed: Array<number | undefined> = [];\n\n    function TestComponent(): JSX.Element {\n      const [value, setValue] = useStateBinding<number>(\"/count\");\n      createEffect(() => {\n        observed.push(value());\n      });\n      return (\n        <button data-testid=\"update\" onClick={() => setValue(2)} />\n      ) as unknown as JSX.Element;\n    }\n\n    const { getByTestId } = render(() => (\n      <StateProvider initialState={{ count: 1 }}>\n        <TestComponent />\n      </StateProvider>\n    ));\n\n    expect(observed).toEqual([1]);\n    getByTestId(\"update\").click();\n    expect(observed).toEqual([1, 2]);\n  });\n\n  it(\"provides initial state to consumers\", () => {\n    const ctx = renderWithState(() => useStateStore(), { a: 1, b: \"hello\" });\n\n    expect(ctx.state).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"provides empty object when no initial state\", () => {\n    const ctx = renderWithState(() => useStateStore());\n\n    expect(ctx.state).toEqual({});\n  });\n\n  it(\"get() retrieves values by path\", () => {\n    const ctx = renderWithState(() => useStateStore(), {\n      user: { name: \"Bob\" },\n    });\n\n    expect(ctx.get(\"/user/name\")).toBe(\"Bob\");\n  });\n\n  it(\"get() returns undefined for missing path\", () => {\n    const ctx = renderWithState(() => useStateStore(), {\n      user: { name: \"Bob\" },\n    });\n\n    expect(ctx.get(\"/user/age\")).toBeUndefined();\n  });\n\n  it(\"set() updates values at path\", () => {\n    const ctx = renderWithState(() => useStateStore(), { x: 0 });\n\n    ctx.set(\"/x\", 42);\n    expect(ctx.get(\"/x\")).toBe(42);\n  });\n\n  it(\"set() creates nested paths\", () => {\n    const ctx = renderWithState(() => useStateStore());\n\n    ctx.set(\"/a/b/c\", \"deep\");\n    expect(ctx.get(\"/a/b/c\")).toBe(\"deep\");\n  });\n\n  it(\"set() calls onStateChange callback\", () => {\n    const onChange = vi.fn();\n    const ctx = renderWithState(\n      () => useStateStore(),\n      {},\n      { onStateChange: onChange },\n    );\n\n    ctx.set(\"/name\", \"Alice\");\n    expect(onChange).toHaveBeenCalledOnce();\n    expect(onChange).toHaveBeenCalledWith([{ path: \"/name\", value: \"Alice\" }]);\n  });\n\n  it(\"update() handles multiple values at once\", () => {\n    const ctx = renderWithState(() => useStateStore());\n\n    ctx.update({ \"/a\": 1, \"/b\": \"hello\" });\n    expect(ctx.get(\"/a\")).toBe(1);\n    expect(ctx.get(\"/b\")).toBe(\"hello\");\n  });\n\n  it(\"update() calls onStateChange with all changes\", () => {\n    const onChange = vi.fn();\n    const ctx = renderWithState(\n      () => useStateStore(),\n      {},\n      { onStateChange: onChange },\n    );\n\n    ctx.update({ \"/a\": 1, \"/b\": 2 });\n    expect(onChange).toHaveBeenCalledOnce();\n    const [changes] = onChange.mock.calls[0]!;\n    expect(changes).toEqual(\n      expect.arrayContaining([\n        { path: \"/a\", value: 1 },\n        { path: \"/b\", value: 2 },\n      ]),\n    );\n  });\n\n  it(\"handles deeply nested state paths\", () => {\n    const ctx = renderWithState(() => useStateStore());\n\n    ctx.set(\"/a/b/c/d\", \"nested\");\n    expect(ctx.get(\"/a/b/c/d\")).toBe(\"nested\");\n  });\n\n  it(\"controlled mode: reads/writes through external StateStore\", () => {\n    const store = createStateStore({ x: 10 });\n    const ctx = renderWithState(() => useStateStore(), {}, { store });\n\n    expect(ctx.get(\"/x\")).toBe(10);\n\n    ctx.set(\"/x\", 99);\n    expect(store.getSnapshot()).toEqual({ x: 99 });\n\n    store.set(\"/x\", 200);\n    expect(store.get(\"/x\")).toBe(200);\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/contexts/state.tsx",
    "content": "import {\n  createContext,\n  useContext,\n  createSignal,\n  createEffect,\n  createMemo,\n  onCleanup,\n  type Accessor,\n  type ParentProps,\n} from \"solid-js\";\nimport {\n  getByPath,\n  createStateStore,\n  type StateModel,\n  type StateStore,\n} from \"@json-render/core\";\nimport { flattenToPointers } from \"@json-render/core/store-utils\";\n\nexport interface StateContextValue {\n  state: StateModel;\n  get: (path: string) => unknown;\n  set: (path: string, value: unknown) => void;\n  update: (updates: Record<string, unknown>) => void;\n  getSnapshot: () => StateModel;\n  subscribeChanges: (\n    listener: (changes: Array<{ path: string; value: unknown }>) => void,\n  ) => () => void;\n}\n\nconst StateContext = createContext<StateContextValue | null>(null);\n\nexport interface StateProviderProps {\n  store?: StateStore;\n  initialState?: StateModel;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n}\n\nfunction computeInitialFlat(\n  isControlled: boolean,\n  initialState: StateModel,\n): Record<string, unknown> | null {\n  if (isControlled) return null;\n  if (Object.keys(initialState).length === 0) return {};\n  return flattenToPointers(initialState);\n}\n\nexport function StateProvider(props: ParentProps<StateProviderProps>) {\n  let internalStore: StateStore | undefined;\n  if (!props.store) {\n    internalStore = createStateStore(props.initialState ?? {});\n  }\n\n  const store = () => props.store ?? internalStore!;\n\n  const initialMode = props.store ? \"controlled\" : \"uncontrolled\";\n  let modeWarned = false;\n\n  if (\n    typeof globalThis !== \"undefined\" &&\n    (globalThis as any).process?.env?.NODE_ENV !== \"production\"\n  ) {\n    createEffect(() => {\n      const currentMode = props.store ? \"controlled\" : \"uncontrolled\";\n      if (currentMode !== initialMode && !modeWarned) {\n        modeWarned = true;\n        console.warn(\n          `StateProvider: switching from ${initialMode} to ${currentMode} mode is not supported.`,\n        );\n      }\n    });\n  }\n\n  let prevInitialState = props.initialState;\n  let prevFlat: Record<string, unknown> | null = computeInitialFlat(\n    !!props.store,\n    props.initialState ?? {},\n  );\n\n  createEffect(() => {\n    if (props.store) return;\n    const initialState = props.initialState ?? {};\n    if (initialState === prevInitialState) return;\n    prevInitialState = initialState;\n    const nextFlat =\n      initialState && Object.keys(initialState).length > 0\n        ? flattenToPointers(initialState)\n        : {};\n    const prevFlatObj = prevFlat ?? {};\n    const allKeys = new Set([\n      ...Object.keys(prevFlatObj),\n      ...Object.keys(nextFlat),\n    ]);\n    const updates: Record<string, unknown> = {};\n    for (const key of allKeys) {\n      if (prevFlatObj[key] !== nextFlat[key]) {\n        updates[key] = key in nextFlat ? nextFlat[key] : undefined;\n      }\n    }\n    prevFlat = nextFlat;\n    if (Object.keys(updates).length > 0) {\n      store().update(updates);\n    }\n  });\n\n  const [state, setState] = createSignal<StateModel>(store().getSnapshot(), {\n    equals: false,\n  });\n  const changeListeners = new Set<\n    (changes: Array<{ path: string; value: unknown }>) => void\n  >();\n\n  const subscribeChanges = (\n    listener: (changes: Array<{ path: string; value: unknown }>) => void,\n  ) => {\n    changeListeners.add(listener);\n    return () => {\n      changeListeners.delete(listener);\n    };\n  };\n\n  const notifyChanges = (changes: Array<{ path: string; value: unknown }>) => {\n    for (const listener of changeListeners) {\n      listener(changes);\n    }\n  };\n\n  createEffect(() => {\n    const s = store();\n    setState(s.getSnapshot());\n    const unsubscribe = s.subscribe(() => {\n      setState(s.getSnapshot());\n    });\n    onCleanup(unsubscribe);\n  });\n\n  const set = (path: string, value: unknown) => {\n    const s = store();\n    const prev = s.getSnapshot();\n    const prevValue = getByPath(prev, path);\n    s.set(path, value);\n    if (prevValue !== value) {\n      const changes = [{ path, value }];\n      notifyChanges(changes);\n      if (!props.store && s.getSnapshot() !== prev) {\n        props.onStateChange?.(changes);\n      }\n    }\n  };\n\n  const update = (updates: Record<string, unknown>) => {\n    const s = store();\n    const prev = s.getSnapshot();\n    s.update(updates);\n    const changes: Array<{ path: string; value: unknown }> = [];\n    for (const [path, value] of Object.entries(updates)) {\n      if (getByPath(prev, path) !== value) {\n        changes.push({ path, value });\n      }\n    }\n    if (changes.length > 0) {\n      notifyChanges(changes);\n      if (!props.store && s.getSnapshot() !== prev) {\n        props.onStateChange?.(changes);\n      }\n    }\n  };\n\n  const get = (path: string) => store().get(path);\n\n  const getSnapshot = () => store().getSnapshot();\n\n  const ctx: StateContextValue = {\n    get state() {\n      return state();\n    },\n    get,\n    set,\n    update,\n    getSnapshot,\n    subscribeChanges,\n  };\n\n  return (\n    <StateContext.Provider value={ctx}>{props.children}</StateContext.Provider>\n  );\n}\n\nexport function useStateStore(): StateContextValue {\n  const ctx = useContext(StateContext);\n  if (!ctx) {\n    throw new Error(\"useStateStore must be used within a StateProvider\");\n  }\n  return ctx;\n}\n\nexport function useStateValue<T>(path: string): Accessor<T | undefined> {\n  const store = useStateStore();\n  return createMemo(() => getByPath(store.state, path) as T | undefined);\n}\n\nexport function useStateBinding<T>(\n  path: string,\n): [Accessor<T | undefined>, (value: T) => void] {\n  const store = useStateStore();\n  const value = createMemo(() => getByPath(store.state, path) as T | undefined);\n  const setValue = (newValue: T) => store.set(path, newValue);\n  return [value, setValue];\n}\n"
  },
  {
    "path": "packages/solid/src/contexts/validation.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { render } from \"@solidjs/testing-library\";\nimport { createEffect, type JSX } from \"solid-js\";\nimport { StateProvider } from \"./state\";\nimport {\n  ValidationProvider,\n  useValidation,\n  useOptionalValidation,\n  useFieldValidation,\n} from \"./validation\";\n\nfunction withProviders<T>(\n  hook: () => T,\n  initialState: Record<string, unknown> = {},\n): T {\n  let result!: T;\n  function TestComponent(): JSX.Element {\n    result = hook();\n    return (<div />) as JSX.Element;\n  }\n  render(() => (\n    <StateProvider initialState={initialState}>\n      <ValidationProvider>\n        <TestComponent />\n      </ValidationProvider>\n    </StateProvider>\n  ));\n  return result;\n}\n\nfunction withStateOnly<T>(hook: () => T): T {\n  let result!: T;\n  function TestComponent(): JSX.Element {\n    result = hook();\n    return (<div />) as JSX.Element;\n  }\n  render(() => (\n    <StateProvider initialState={{}}>\n      <TestComponent />\n    </StateProvider>\n  ));\n  return result;\n}\n\nfunction withBothContexts(\n  initialState: Record<string, unknown> = {},\n  fieldPath: string = \"/name\",\n  config?: { checks: Array<{ type: string; message: string }> },\n) {\n  let validationCtx!: ReturnType<typeof useValidation>;\n  let fieldCtx!: ReturnType<typeof useFieldValidation>;\n  function TestComponent(): JSX.Element {\n    validationCtx = useValidation();\n    fieldCtx = useFieldValidation(fieldPath, config);\n    return (<div />) as JSX.Element;\n  }\n  render(() => (\n    <StateProvider initialState={initialState}>\n      <ValidationProvider>\n        <TestComponent />\n      </ValidationProvider>\n    </StateProvider>\n  ));\n  return { validationCtx, fieldCtx };\n}\n\ndescribe(\"ValidationProvider — provide/inject\", () => {\n  it(\"useValidation() throws outside a provider\", () => {\n    expect(() => useValidation()).toThrow(\n      \"useValidation must be used within a ValidationProvider\",\n    );\n  });\n});\n\ndescribe(\"useOptionalValidation\", () => {\n  it(\"returns null outside ValidationProvider\", () => {\n    const result = withStateOnly(() => useOptionalValidation());\n    expect(result).toBeNull();\n  });\n\n  it(\"returns context inside ValidationProvider\", () => {\n    const result = withProviders(() => useOptionalValidation());\n    expect(result).not.toBeNull();\n    expect(typeof result!.validate).toBe(\"function\");\n    expect(typeof result!.validateAll).toBe(\"function\");\n  });\n});\n\ndescribe(\"useFieldValidation — lifecycle\", () => {\n  it(\"validate() with empty required field returns valid:false and errors\", () => {\n    const { fieldCtx } = withBothContexts({ name: \"\" }, \"/name\", {\n      checks: [{ type: \"required\", message: \"Name is required\" }],\n    });\n\n    const result = fieldCtx.validate();\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain(\"Name is required\");\n    expect(fieldCtx.errors()).toContain(\"Name is required\");\n    expect(fieldCtx.isValid()).toBe(false);\n  });\n\n  it(\"validate() with valid value returns valid:true and no errors\", () => {\n    const { fieldCtx } = withBothContexts({ name: \"Alice\" }, \"/name\", {\n      checks: [{ type: \"required\", message: \"Name is required\" }],\n    });\n\n    const result = fieldCtx.validate();\n    expect(result.valid).toBe(true);\n    expect(result.errors).toHaveLength(0);\n    expect(fieldCtx.errors()).toHaveLength(0);\n    expect(fieldCtx.isValid()).toBe(true);\n  });\n\n  it(\"touch() sets touched:true in fieldStates\", () => {\n    const { validationCtx, fieldCtx } = withBothContexts({}, \"/email\");\n\n    fieldCtx.touch();\n    expect(validationCtx.fieldStates[\"/email\"]?.touched).toBe(true);\n  });\n\n  it(\"clear() resets field state from validation context\", () => {\n    const { validationCtx, fieldCtx } = withBothContexts(\n      { email: \"\" },\n      \"/email\",\n      { checks: [{ type: \"required\", message: \"Required\" }] },\n    );\n\n    fieldCtx.validate();\n    fieldCtx.clear();\n    expect(validationCtx.fieldStates[\"/email\"]).toBeUndefined();\n  });\n\n  it(\"useFieldValidation accessors stay reactive after validate/touch\", () => {\n    const observedErrors: string[][] = [];\n    const observedTouched: boolean[] = [];\n\n    function TestComponent(): JSX.Element {\n      const field = useFieldValidation(\"/email\", {\n        checks: [{ type: \"required\", message: \"Required\" }],\n      });\n\n      createEffect(() => {\n        observedErrors.push(field.errors());\n      });\n\n      createEffect(() => {\n        observedTouched.push(field.state().touched);\n      });\n\n      return (\n        <>\n          <button data-testid=\"validate\" onClick={() => field.validate()} />\n          <button data-testid=\"touch\" onClick={() => field.touch()} />\n        </>\n      ) as JSX.Element;\n    }\n\n    const { getByTestId } = render(() => (\n      <StateProvider initialState={{ email: \"\" }}>\n        <ValidationProvider>\n          <TestComponent />\n        </ValidationProvider>\n      </StateProvider>\n    ));\n\n    expect(observedErrors).toEqual([[]]);\n    expect(observedTouched).toEqual([false]);\n\n    getByTestId(\"validate\").click();\n    expect(observedErrors).toEqual([[], [\"Required\"]]);\n    expect(observedTouched).toEqual([false, true]);\n\n    getByTestId(\"touch\").click();\n    expect(observedTouched).toEqual([false, true, true]);\n  });\n});\n\ndescribe(\"validateAll\", () => {\n  it(\"returns true when all registered fields pass\", () => {\n    const { validationCtx } = withBothContexts({ name: \"Alice\" }, \"/name\", {\n      checks: [{ type: \"required\", message: \"Required\" }],\n    });\n\n    expect(validationCtx.validateAll()).toBe(true);\n  });\n\n  it(\"returns false when any field fails\", () => {\n    const { validationCtx } = withBothContexts({ name: \"\" }, \"/name\", {\n      checks: [{ type: \"required\", message: \"Required\" }],\n    });\n\n    expect(validationCtx.validateAll()).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/contexts/validation.tsx",
    "content": "import {\n  createContext,\n  useContext,\n  createSignal,\n  createEffect,\n  createMemo,\n  type Accessor,\n  type ParentProps,\n} from \"solid-js\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface FieldValidationState {\n  touched: boolean;\n  validated: boolean;\n  result: ValidationResult | null;\n}\n\nexport interface ValidationContextValue {\n  customFunctions: Record<string, ValidationFunction>;\n  fieldStates: Record<string, FieldValidationState>;\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  touch: (path: string) => void;\n  clear: (path: string) => void;\n  validateAll: () => boolean;\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst ValidationContext = createContext<ValidationContextValue | null>(null);\n\nexport interface ValidationProviderProps {\n  customFunctions?: Record<string, ValidationFunction>;\n}\n\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n\n  if (a.validateOn !== b.validateOn) return false;\n\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n\n  return true;\n}\n\nexport function ValidationProvider(\n  props: ParentProps<ValidationProviderProps>,\n) {\n  const { getSnapshot } = useStateStore();\n  const customFunctions = () => props.customFunctions ?? {};\n\n  const [fieldStates, setFieldStates] = createSignal<\n    Record<string, FieldValidationState>\n  >({});\n  let fieldStatesRef: Record<string, FieldValidationState> = {};\n\n  const [fieldConfigs, setFieldConfigs] = createSignal<\n    Record<string, ValidationConfig>\n  >({});\n\n  const registerField = (path: string, config: ValidationConfig) => {\n    setFieldConfigs((prev) => {\n      const existing = prev[path];\n      if (existing && validationConfigEqual(existing, config)) {\n        return prev;\n      }\n      return { ...prev, [path]: config };\n    });\n  };\n\n  const validate = (\n    path: string,\n    config: ValidationConfig,\n  ): ValidationResult => {\n    const currentState = getSnapshot();\n    const segments = path.split(\"/\").filter(Boolean);\n    let value: unknown = currentState;\n    for (const seg of segments) {\n      if (value != null && typeof value === \"object\") {\n        value = (value as Record<string, unknown>)[seg];\n      } else {\n        value = undefined;\n        break;\n      }\n    }\n    const result = runValidation(config, {\n      value,\n      stateModel: currentState,\n      customFunctions: customFunctions(),\n    });\n\n    const newFieldState: FieldValidationState = {\n      touched: fieldStatesRef[path]?.touched ?? true,\n      validated: true,\n      result,\n    };\n    fieldStatesRef = {\n      ...fieldStatesRef,\n      [path]: newFieldState,\n    };\n    setFieldStates(fieldStatesRef);\n\n    return result;\n  };\n\n  const touch = (path: string) => {\n    fieldStatesRef = {\n      ...fieldStatesRef,\n      [path]: {\n        ...fieldStatesRef[path],\n        touched: true,\n        validated: fieldStatesRef[path]?.validated ?? false,\n        result: fieldStatesRef[path]?.result ?? null,\n      },\n    };\n    setFieldStates(fieldStatesRef);\n  };\n\n  const clear = (path: string) => {\n    const { [path]: _, ...rest } = fieldStatesRef;\n    fieldStatesRef = rest;\n    setFieldStates(rest);\n  };\n\n  const validateAll = () => {\n    let allValid = true;\n\n    for (const [path, config] of Object.entries(fieldConfigs())) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n\n    return allValid;\n  };\n\n  const value: ValidationContextValue = {\n    get customFunctions() {\n      return customFunctions();\n    },\n    get fieldStates() {\n      fieldStates();\n      return fieldStatesRef;\n    },\n    validate,\n    touch,\n    clear,\n    validateAll,\n    registerField,\n  };\n\n  return (\n    <ValidationContext.Provider value={value}>\n      {props.children}\n    </ValidationContext.Provider>\n  );\n}\n\nexport function useValidation(): ValidationContextValue {\n  const ctx = useContext(ValidationContext);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\nexport function useOptionalValidation(): ValidationContextValue | null {\n  return useContext(ValidationContext);\n}\n\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: Accessor<FieldValidationState>;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: Accessor<string[]>;\n  isValid: Accessor<boolean>;\n} {\n  const validation = useValidation();\n\n  createEffect(() => {\n    if (path && config) {\n      validation.registerField(path, config);\n    }\n  });\n\n  const state = createMemo<FieldValidationState>(() => {\n    const current = validation.fieldStates[path];\n    return (\n      current ?? {\n        touched: false,\n        validated: false,\n        result: null,\n      }\n    );\n  });\n\n  const validate = () => validation.validate(path, config ?? { checks: [] });\n  const touch = () => validation.touch(path);\n  const clear = () => validation.clear(path);\n  const errors = createMemo(() => state().result?.errors ?? []);\n  const isValid = createMemo(() => state().result?.valid ?? true);\n\n  return {\n    state,\n    validate,\n    touch,\n    clear,\n    errors,\n    isValid,\n  };\n}\n"
  },
  {
    "path": "packages/solid/src/contexts/visibility.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { render } from \"@solidjs/testing-library\";\nimport type { JSX } from \"solid-js\";\nimport { VisibilityProvider, useVisibility, useIsVisible } from \"./visibility\";\nimport { StateProvider } from \"./state\";\n\nfunction renderWithVisibility<T>(\n  hook: () => T,\n  data: Record<string, unknown> = {},\n): T {\n  let result!: T;\n  function TestComponent(): JSX.Element {\n    result = hook();\n    return (<div />) as unknown as JSX.Element;\n  }\n  render(() => (\n    <StateProvider initialState={data}>\n      <VisibilityProvider>\n        <TestComponent />\n      </VisibilityProvider>\n    </StateProvider>\n  ));\n  return result;\n}\n\ndescribe(\"useVisibility\", () => {\n  it(\"provides isVisible function\", () => {\n    const ctx = renderWithVisibility(() => useVisibility());\n\n    expect(typeof ctx.isVisible).toBe(\"function\");\n  });\n\n  it(\"provides visibility context with stateModel\", () => {\n    const ctx = renderWithVisibility(() => useVisibility(), { test: true });\n\n    expect(ctx.ctx.stateModel).toEqual({ test: true });\n  });\n});\n\ndescribe(\"useIsVisible\", () => {\n  it(\"returns true for undefined condition\", () => {\n    const result = renderWithVisibility(() => useIsVisible(undefined));\n\n    expect(result).toBe(true);\n  });\n\n  it(\"returns true for true condition\", () => {\n    const result = renderWithVisibility(() => useIsVisible(true));\n\n    expect(result).toBe(true);\n  });\n\n  it(\"returns false for false condition\", () => {\n    const result = renderWithVisibility(() => useIsVisible(false));\n\n    expect(result).toBe(false);\n  });\n\n  it(\"evaluates $state conditions against data\", () => {\n    const trueResult = renderWithVisibility(\n      () => useIsVisible({ $state: \"/isVisible\" }),\n      { isVisible: true },\n    );\n    expect(trueResult).toBe(true);\n\n    const falseResult = renderWithVisibility(\n      () => useIsVisible({ $state: \"/isVisible\" }),\n      { isVisible: false },\n    );\n    expect(falseResult).toBe(false);\n  });\n\n  it(\"evaluates equality conditions\", () => {\n    const result = renderWithVisibility(\n      () => useIsVisible({ $state: \"/count\", eq: 1 }),\n      { count: 1 },\n    );\n\n    expect(result).toBe(true);\n  });\n\n  it(\"evaluates array conditions (implicit AND)\", () => {\n    const result = renderWithVisibility(\n      () =>\n        useIsVisible([\n          { $state: \"/user/isAdmin\" },\n          { $state: \"/count\", eq: 5 },\n        ]),\n      { user: { isAdmin: true }, count: 5 },\n    );\n\n    expect(result).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/contexts/visibility.tsx",
    "content": "import { createContext, useContext, type ParentProps } from \"solid-js\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\nexport interface VisibilityContextValue {\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  ctx: CoreVisibilityContext;\n}\n\nconst VisibilityContext = createContext<VisibilityContextValue | null>(null);\n\nexport type VisibilityProviderProps = ParentProps;\n\nexport function VisibilityProvider(props: VisibilityProviderProps) {\n  const stateStore = useStateStore();\n\n  const visibilityCtx: CoreVisibilityContext = {\n    get stateModel() {\n      return stateStore.state;\n    },\n  };\n\n  const value: VisibilityContextValue = {\n    isVisible: (condition: VisibilityCondition | undefined) =>\n      evaluateVisibility(condition, visibilityCtx),\n    ctx: visibilityCtx,\n  };\n\n  return (\n    <VisibilityContext.Provider value={value}>\n      {props.children}\n    </VisibilityContext.Provider>\n  );\n}\n\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = useContext(VisibilityContext);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): boolean {\n  const { isVisible } = useVisibility();\n  return isVisible(condition);\n}\n"
  },
  {
    "path": "packages/solid/src/dynamic-forms.test.tsx",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, fireEvent, waitFor } from \"@solidjs/testing-library\";\nimport { createSignal } from \"solid-js\";\nimport { defineCatalog, type Spec } from \"@json-render/core\";\nimport {\n  JSONUIProvider,\n  Renderer,\n  defineRegistry,\n  type ComponentRenderProps,\n} from \"./renderer\";\nimport type { ComponentFn } from \"./catalog-types\";\nimport { useStateStore } from \"./contexts/state\";\nimport { useFieldValidation } from \"./contexts/validation\";\nimport { useBoundProp } from \"./hooks\";\nimport { schema as solidSchema } from \"./schema\";\nimport { z } from \"zod\";\n\nconst exampleCatalog = defineCatalog(solidSchema, {\n  components: {\n    Stack: {\n      props: z.object({\n        gap: z.number().optional(),\n        padding: z.number().optional(),\n        direction: z.enum([\"vertical\", \"horizontal\"]).optional(),\n        align: z.enum([\"start\", \"center\", \"end\"]).optional(),\n      }),\n      slots: [\"default\"],\n      description:\n        \"Layout container that stacks children vertically or horizontally\",\n    },\n    Card: {\n      props: z.object({\n        title: z.string().optional(),\n        subtitle: z.string().optional(),\n      }),\n      slots: [\"default\"],\n      description: \"A card container with optional title and subtitle\",\n    },\n    Text: {\n      props: z.object({\n        content: z.string(),\n        size: z.enum([\"sm\", \"md\", \"lg\", \"xl\"]).optional(),\n        weight: z.enum([\"normal\", \"medium\", \"bold\"]).optional(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"Displays a text string\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        variant: z.enum([\"primary\", \"secondary\", \"danger\"]).optional(),\n        disabled: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A clickable button that emits a 'press' event\",\n    },\n    Badge: {\n      props: z.object({\n        label: z.string(),\n        color: z.string().optional(),\n      }),\n      slots: [],\n      description: \"A small badge/tag label\",\n    },\n    ListItem: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().optional(),\n        completed: z.boolean().optional(),\n      }),\n      slots: [],\n      description: \"A single item in a list\",\n    },\n    RendererTabs: {\n      props: z.object({ renderer: z.string() }),\n      slots: [],\n      description:\n        \"Segmented tab control for switching between Vue, React, Svelte, and Solid renderers\",\n    },\n    RendererBadge: {\n      props: z.object({ renderer: z.string() }),\n      slots: [],\n      description: \"Badge indicating which renderer is currently active\",\n    },\n  },\n  actions: {\n    increment: {\n      params: z.object({}),\n      description: \"Increment the counter by 1\",\n    },\n    decrement: {\n      params: z.object({}),\n      description: \"Decrement the counter by 1\",\n    },\n    reset: {\n      params: z.object({}),\n      description: \"Reset the counter to 0\",\n    },\n    toggleItem: {\n      params: z.object({ index: z.number() }),\n      description: \"Toggle the completed state of a todo item\",\n    },\n    switchToVue: {\n      params: z.object({}),\n      description: \"Switch to the Vue renderer\",\n    },\n    switchToReact: {\n      params: z.object({}),\n      description: \"Switch to the React renderer\",\n    },\n    switchToSvelte: {\n      params: z.object({}),\n      description: \"Switch to the Svelte renderer\",\n    },\n    switchToSolid: {\n      params: z.object({}),\n      description: \"Switch to the Solid renderer\",\n    },\n  },\n});\n\nconst definedStack: ComponentFn<typeof exampleCatalog, \"Stack\"> = (ctx) => (\n  <div>{ctx.children}</div>\n);\n\nconst definedButton: ComponentFn<typeof exampleCatalog, \"Button\"> = (ctx) => (\n  <button data-testid=\"defined-btn\" onClick={() => ctx.emit(\"press\")}>\n    {ctx.props.label}\n  </button>\n);\n\nconst definedText: ComponentFn<typeof exampleCatalog, \"Text\"> = (ctx) => (\n  <span data-testid=\"defined-text\">{ctx.props.content}</span>\n);\n\nconst definedCard: ComponentFn<typeof exampleCatalog, \"Card\"> = (ctx) => (\n  <div>{ctx.children}</div>\n);\n\nconst definedBadge: ComponentFn<typeof exampleCatalog, \"Badge\"> = (ctx) => (\n  <span>{ctx.props.label}</span>\n);\n\nconst definedListItem: ComponentFn<typeof exampleCatalog, \"ListItem\"> = (\n  ctx,\n) => <div>{ctx.props.title}</div>;\n\nconst definedRendererBadge: ComponentFn<\n  typeof exampleCatalog,\n  \"RendererBadge\"\n> = (ctx) => <span>{ctx.props.renderer}</span>;\n\nconst definedRendererTabs: ComponentFn<\n  typeof exampleCatalog,\n  \"RendererTabs\"\n> = () => <div />;\n\nconst exampleComponents = {\n  Stack: definedStack,\n  Button: definedButton,\n  Text: definedText,\n  Card: definedCard,\n  Badge: definedBadge,\n  ListItem: definedListItem,\n  RendererBadge: definedRendererBadge,\n  RendererTabs: definedRendererTabs,\n};\n\nfunction Button(props: ComponentRenderProps<{ label: string }>) {\n  return (\n    <button data-testid=\"btn\" onClick={() => props.emit(\"press\")}>\n      {props.element.props.label}\n    </button>\n  );\n}\n\nfunction Text(props: ComponentRenderProps<{ text: unknown }>) {\n  const display = () => {\n    const v = props.element.props.text;\n    if (v == null) return \"\";\n    return typeof v === \"string\" ? v : JSON.stringify(v);\n  };\n  return <span data-testid=\"text\">{display()}</span>;\n}\n\nfunction InputField(\n  props: ComponentRenderProps<{\n    label?: string;\n    value?: string;\n    checks?: Array<{\n      type: string;\n      message: string;\n      args?: Record<string, unknown>;\n    }>;\n  }>,\n) {\n  const elementProps = () => props.element.props;\n  const [boundValue, setBoundValue] = useBoundProp<string>(\n    elementProps().value as string | undefined,\n    props.bindings?.value,\n  );\n  const [localValue, setLocalValue] = createSignal(\"\");\n  const isBound = () => !!props.bindings?.value;\n  const value = () => (isBound() ? (boundValue ?? \"\") : localValue());\n  const setValue = (v: string) =>\n    isBound() ? setBoundValue(v) : setLocalValue(v);\n\n  const hasValidation = () =>\n    !!(props.bindings?.value && elementProps().checks?.length);\n  const config = () =>\n    hasValidation() ? { checks: elementProps().checks ?? [] } : undefined;\n  const { errors } = useFieldValidation(props.bindings?.value ?? \"\", config());\n\n  return (\n    <div>\n      {elementProps().label && <label>{elementProps().label}</label>}\n      <input\n        data-testid=\"input\"\n        value={value()}\n        onInput={(e) => setValue(e.currentTarget.value)}\n      />\n      {errors().length > 0 && (\n        <span data-testid=\"input-error\">{errors()[0]}</span>\n      )}\n    </div>\n  );\n}\n\nfunction SelectField(\n  props: ComponentRenderProps<{ label?: string; value?: string }>,\n) {\n  const [boundValue] = useBoundProp<string>(\n    props.element.props.value as string | undefined,\n    props.bindings?.value,\n  );\n  return <span data-testid=\"select-value\">{boundValue ?? \"\"}</span>;\n}\n\nfunction ValidatedSelect(\n  props: ComponentRenderProps<{\n    label?: string;\n    name?: string;\n    options?: string[];\n    placeholder?: string;\n    value?: string;\n    checks?: Array<{\n      type: string;\n      message: string;\n      args?: Record<string, unknown>;\n    }>;\n    validateOn?: \"change\" | \"blur\" | \"submit\";\n  }>,\n) {\n  const elementProps = () => props.element.props;\n  const [boundValue, setBoundValue] = useBoundProp<string>(\n    elementProps().value as string | undefined,\n    props.bindings?.value,\n  );\n  const [localValue, setLocalValue] = createSignal(\"\");\n  const isBound = () => !!props.bindings?.value;\n  const value = () => (isBound() ? (boundValue ?? \"\") : localValue());\n  const setValue = (v: string) =>\n    isBound() ? setBoundValue(v) : setLocalValue(v);\n  const validateOn = () => elementProps().validateOn ?? \"change\";\n\n  const hasValidation = () =>\n    !!(props.bindings?.value && elementProps().checks?.length);\n  const config = () =>\n    hasValidation()\n      ? { checks: elementProps().checks ?? [], validateOn: validateOn() }\n      : undefined;\n  const { errors, validate } = useFieldValidation(\n    props.bindings?.value ?? \"\",\n    config(),\n  );\n\n  const options = () => elementProps().options ?? [];\n  const name = () => elementProps().name ?? \"default\";\n\n  return (\n    <div>\n      {elementProps().label && <label>{elementProps().label}</label>}\n      <select\n        data-testid={`select-${name()}`}\n        value={value()}\n        onChange={(e) => {\n          setValue(e.currentTarget.value);\n          if (hasValidation() && validateOn() === \"change\") validate();\n          props.emit(\"change\");\n        }}\n      >\n        <option value=\"\">{elementProps().placeholder ?? \"Select...\"}</option>\n        {options().map((opt) => (\n          <option value={opt}>{opt}</option>\n        ))}\n      </select>\n      {errors().length > 0 && (\n        <span data-testid={`select-error-${name()}`}>{errors()[0]}</span>\n      )}\n    </div>\n  );\n}\n\nfunction Stack(props: ComponentRenderProps<Record<string, unknown>>) {\n  return <div data-testid=\"stack\">{props.children}</div>;\n}\n\nlet probeGetSnapshot: (() => Record<string, unknown>) | undefined;\n\nfunction StateProbe() {\n  const ctx = useStateStore();\n  probeGetSnapshot = ctx.getSnapshot;\n  return <div data-testid=\"state-probe\" />;\n}\n\nconst registry = { Button, Text, Input: InputField, Select: SelectField };\n\nfunction getState(): Record<string, unknown> {\n  return probeGetSnapshot!();\n}\n\n// =============================================================================\n// $computed expressions in rendering\n// =============================================================================\n\ndescribe(\"$computed expressions in rendering\", () => {\n  it(\"resolves a $computed prop using provided functions\", () => {\n    const spec: Spec = {\n      state: { first: \"Jane\", last: \"Doe\" },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: {\n              $computed: \"fullName\",\n              args: {\n                first: { $state: \"/first\" },\n                last: { $state: \"/last\" },\n              },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    const functions = {\n      fullName: (args: Record<string, unknown>) => `${args.first} ${args.last}`,\n    };\n\n    render(() => (\n      <JSONUIProvider\n        registry={registry}\n        initialState={spec.state}\n        functions={functions}\n      >\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>\n    ));\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"Jane Doe\");\n  });\n\n  it(\"renders gracefully when functions prop is omitted\", () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const spec: Spec = {\n      state: {},\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: { $computed: \"missing\" },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>\n    ));\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"\");\n    warnSpy.mockRestore();\n  });\n});\n\n// =============================================================================\n// $template expressions in rendering\n// =============================================================================\n\ndescribe(\"$template expressions in rendering\", () => {\n  it(\"interpolates state values into a template string\", () => {\n    const spec: Spec = {\n      state: { user: { name: \"Alice\" }, count: 3 },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: {\n              $template: \"Hello, ${/user/name}! You have ${/count} messages.\",\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>\n    ));\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\n      \"Hello, Alice! You have 3 messages.\",\n    );\n  });\n\n  it(\"resolves missing paths to empty string\", () => {\n    const spec: Spec = {\n      state: {},\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: { $template: \"Hi ${/name}!\" },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={registry} initialState={spec.state}>\n        <Renderer spec={spec} registry={registry} />\n      </JSONUIProvider>\n    ));\n\n    expect(screen.getByTestId(\"text\").textContent).toBe(\"Hi !\");\n  });\n});\n\n// =============================================================================\n// Watchers\n// =============================================================================\n\ndescribe(\"watchers (watch field)\", () => {\n  it(\"does not fire on initial render, fires when watched state changes\", async () => {\n    probeGetSnapshot = undefined;\n    const loadCities = vi.fn(async (_params: Record<string, unknown>) => {});\n\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { country: \"\" }, citiesLoaded: false },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Set Country\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/form/country\", value: \"US\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Select\",\n          props: { value: { $state: \"/form/country\" } },\n          watch: {\n            \"/form/country\": {\n              action: \"loadCities\",\n              params: { country: { $state: \"/form/country\" } },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider\n        registry={reg}\n        initialState={spec.state}\n        handlers={{ loadCities }}\n      >\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    expect(loadCities).not.toHaveBeenCalled();\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      expect(loadCities).toHaveBeenCalledTimes(1);\n    });\n    expect(loadCities).toHaveBeenCalledWith(\n      expect.objectContaining({ country: \"US\" }),\n    );\n  });\n\n  it(\"fires multiple action bindings on the same watch path\", async () => {\n    probeGetSnapshot = undefined;\n    const action1 = vi.fn();\n    const action2 = vi.fn();\n\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { value: \"a\" },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Change\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/value\", value: \"b\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Text\",\n          props: { text: { $state: \"/value\" } },\n          watch: {\n            \"/value\": [{ action: \"action1\" }, { action: \"action2\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider\n        registry={reg}\n        initialState={spec.state}\n        handlers={{ action1, action2 }}\n      >\n        <Renderer spec={spec} registry={reg} />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      expect(action1).toHaveBeenCalledTimes(1);\n      expect(action2).toHaveBeenCalledTimes(1);\n    });\n  });\n});\n\ndescribe(\"defineRegistry reactivity\", () => {\n  it(\"updates $state-backed props after setState actions\", async () => {\n    const { registry: definedRegistry } = defineRegistry(exampleCatalog, {\n      components: exampleComponents,\n      actions: {\n        increment: async () => {},\n        decrement: async () => {},\n        reset: async () => {},\n        toggleItem: async () => {},\n        switchToVue: async () => {},\n        switchToReact: async () => {},\n        switchToSvelte: async () => {},\n        switchToSolid: async () => {},\n      },\n    });\n\n    const spec: Spec = {\n      state: { value: \"a\" },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"text\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Change\" },\n          on: {\n            press: {\n              action: \"setState\",\n              params: { statePath: \"/value\", value: \"b\" },\n            },\n          },\n          children: [],\n        },\n        text: {\n          type: \"Text\",\n          props: { content: { $state: \"/value\" } },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={definedRegistry} initialState={spec.state}>\n        <Renderer spec={spec} registry={definedRegistry} />\n      </JSONUIProvider>\n    ));\n\n    expect(screen.getByTestId(\"defined-text\").textContent).toBe(\"a\");\n\n    fireEvent.click(screen.getByTestId(\"defined-btn\"));\n\n    await waitFor(() => {\n      expect(screen.getByTestId(\"defined-text\").textContent).toBe(\"b\");\n    });\n  });\n});\n\n// =============================================================================\n// validateForm action\n// =============================================================================\n\ndescribe(\"validateForm action\", () => {\n  it(\"writes { valid: false } when a required field is empty\", async () => {\n    probeGetSnapshot = undefined;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { email: \"\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getState();\n      expect(state.result).toEqual({\n        valid: false,\n        errors: { \"/form/email\": [\"Email is required\"] },\n      });\n    });\n  });\n\n  it(\"writes { valid: true } when all fields pass validation\", async () => {\n    probeGetSnapshot = undefined;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { email: \"test@example.com\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getState();\n      expect(state.result).toEqual({ valid: true, errors: {} });\n    });\n  });\n\n  it(\"defaults to /formValidation when no statePath is provided\", async () => {\n    probeGetSnapshot = undefined;\n    const reg = { ...registry, Stack };\n\n    const spec: Spec = {\n      state: { form: { name: \"filled\" } },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"nameInput\", \"submitBtn\"],\n        },\n        nameInput: {\n          type: \"Input\",\n          props: {\n            label: \"Name\",\n            value: { $bindState: \"/form/name\" },\n            checks: [{ type: \"required\", message: \"Required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [{ action: \"validateForm\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={reg} initialState={spec.state}>\n        <Renderer spec={spec} registry={reg} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.click(screen.getByTestId(\"btn\"));\n\n    await waitFor(() => {\n      const state = getState();\n      expect(state.formValidation).toEqual({ valid: true, errors: {} });\n    });\n  });\n});\n\n// =============================================================================\n// Select validate-on-change timing (#151)\n// =============================================================================\n\ndescribe(\"Select validate-on-change sees the new value, not the stale value\", () => {\n  const regWithSelect = {\n    ...registry,\n    Stack,\n    Select: ValidatedSelect,\n  };\n\n  it(\"does not show 'required' error when selecting the first value\", async () => {\n    probeGetSnapshot = undefined;\n    const spec: Spec = {\n      state: { form: { country: \"\" } },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"countrySelect\"],\n        },\n        countrySelect: {\n          type: \"Select\",\n          props: {\n            label: \"Country\",\n            name: \"country\",\n            options: [\"US\", \"Canada\", \"UK\"],\n            placeholder: \"Choose a country\",\n            value: { $bindState: \"/form/country\" },\n            checks: [{ type: \"required\", message: \"Country is required\" }],\n            validateOn: \"change\",\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={regWithSelect} initialState={spec.state}>\n        <Renderer spec={spec} registry={regWithSelect} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.change(screen.getByTestId(\"select-country\"), {\n      target: { value: \"US\" },\n    });\n\n    await waitFor(() => {\n      const state = getState();\n      expect((state.form as Record<string, unknown>).country).toBe(\"US\");\n    });\n\n    expect(screen.queryByTestId(\"select-error-country\")).toBeNull();\n  });\n\n  it(\"does not show 'required' error when selecting the first city after country change resets it\", async () => {\n    probeGetSnapshot = undefined;\n    const spec: Spec = {\n      state: {\n        form: { country: \"US\", city: \"\" },\n        availableCities: [\"New York\", \"Chicago\"],\n      },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"citySelect\"],\n        },\n        citySelect: {\n          type: \"Select\",\n          props: {\n            label: \"City\",\n            name: \"city\",\n            options: [\"New York\", \"Chicago\"],\n            placeholder: \"Select a city\",\n            value: { $bindState: \"/form/city\" },\n            checks: [{ type: \"required\", message: \"City is required\" }],\n            validateOn: \"change\",\n          },\n          children: [],\n        },\n      },\n    };\n\n    render(() => (\n      <JSONUIProvider registry={regWithSelect} initialState={spec.state}>\n        <Renderer spec={spec} registry={regWithSelect} />\n        <StateProbe />\n      </JSONUIProvider>\n    ));\n\n    fireEvent.change(screen.getByTestId(\"select-city\"), {\n      target: { value: \"New York\" },\n    });\n\n    await waitFor(() => {\n      const state = getState();\n      expect((state.form as Record<string, unknown>).city).toBe(\"New York\");\n    });\n\n    expect(screen.queryByTestId(\"select-error-city\")).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/hooks.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { flatToTree, buildSpecFromParts, getTextFromParts } from \"./hooks\";\n\ndescribe(\"flatToTree\", () => {\n  it(\"converts array of elements to tree structure\", () => {\n    const elements = [\n      { key: \"container\", type: \"stack\", props: {}, parentKey: null },\n      {\n        key: \"text1\",\n        type: \"text\",\n        props: { content: \"Hello\" },\n        parentKey: \"container\",\n      },\n      {\n        key: \"text2\",\n        type: \"text\",\n        props: { content: \"World\" },\n        parentKey: \"container\",\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"container\");\n    expect(Object.keys(tree.elements)).toHaveLength(3);\n    expect(tree.elements[\"container\"]).toBeDefined();\n    expect(tree.elements[\"text1\"]).toBeDefined();\n    expect(tree.elements[\"text2\"]).toBeDefined();\n  });\n\n  it(\"builds parent-child relationships\", () => {\n    const elements = [\n      { key: \"root\", type: \"stack\", props: {}, parentKey: null },\n      { key: \"child1\", type: \"text\", props: {}, parentKey: \"root\" },\n      { key: \"child2\", type: \"text\", props: {}, parentKey: \"root\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"root\"]?.children).toHaveLength(2);\n    expect(tree.elements[\"root\"]?.children).toContain(\"child1\");\n    expect(tree.elements[\"root\"]?.children).toContain(\"child2\");\n  });\n\n  it(\"handles single root element\", () => {\n    const elements = [\n      {\n        key: \"only\",\n        type: \"text\",\n        props: { content: \"Single\" },\n        parentKey: null,\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"only\");\n    expect(Object.keys(tree.elements)).toHaveLength(1);\n  });\n\n  it(\"handles deeply nested elements\", () => {\n    const elements = [\n      { key: \"level0\", type: \"stack\", props: {}, parentKey: null },\n      { key: \"level1\", type: \"stack\", props: {}, parentKey: \"level0\" },\n      { key: \"level2\", type: \"stack\", props: {}, parentKey: \"level1\" },\n      { key: \"level3\", type: \"text\", props: {}, parentKey: \"level2\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.root).toBe(\"level0\");\n    expect(tree.elements[\"level0\"]?.children).toContain(\"level1\");\n    expect(tree.elements[\"level1\"]?.children).toContain(\"level2\");\n    expect(tree.elements[\"level2\"]?.children).toContain(\"level3\");\n  });\n\n  it(\"preserves element props\", () => {\n    const elements = [\n      {\n        key: \"btn\",\n        type: \"button\",\n        props: { label: \"Click me\", variant: \"primary\" },\n        parentKey: null,\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"btn\"]?.props).toEqual({\n      label: \"Click me\",\n      variant: \"primary\",\n    });\n  });\n\n  it(\"preserves visibility conditions\", () => {\n    const elements = [\n      {\n        key: \"conditional\",\n        type: \"text\",\n        props: {},\n        parentKey: null,\n        visible: { $state: \"/isVisible\" },\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"conditional\"]?.visible).toEqual({\n      $state: \"/isVisible\",\n    });\n  });\n\n  it(\"handles elements with undefined parentKey as root\", () => {\n    const elements = [\n      { key: \"root\", type: \"stack\", props: {} } as {\n        key: string;\n        type: string;\n        props: Record<string, unknown>;\n        parentKey?: string | null;\n      },\n    ];\n\n    const tree = flatToTree(elements);\n\n    // Elements without parentKey should not become root (only null parentKey)\n    // This tests the edge case\n    expect(tree.elements[\"root\"]).toBeDefined();\n  });\n\n  it(\"handles empty elements array\", () => {\n    const tree = flatToTree([]);\n\n    expect(tree.root).toBe(\"\");\n    expect(Object.keys(tree.elements)).toHaveLength(0);\n  });\n\n  it(\"handles multiple children correctly\", () => {\n    const elements = [\n      { key: \"parent\", type: \"grid\", props: {}, parentKey: null },\n      { key: \"a\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"b\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"c\", type: \"card\", props: {}, parentKey: \"parent\" },\n      { key: \"d\", type: \"card\", props: {}, parentKey: \"parent\" },\n    ];\n\n    const tree = flatToTree(elements);\n\n    expect(tree.elements[\"parent\"]?.children).toHaveLength(4);\n    expect(tree.elements[\"parent\"]?.children).toEqual([\"a\", \"b\", \"c\", \"d\"]);\n  });\n});\n\n// =============================================================================\n// buildSpecFromParts\n// =============================================================================\n\ndescribe(\"buildSpecFromParts\", () => {\n  it(\"returns null when no data-spec parts are present\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello there\" },\n      { type: \"text\", text: \"How can I help?\" },\n    ];\n    expect(buildSpecFromParts(parts)).toBeNull();\n  });\n\n  it(\"builds a spec from patch parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Card\", props: { title: \"Hello\" }, children: [] },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"main\");\n    expect(spec!.elements.main).toEqual({\n      type: \"Card\",\n      props: { title: \"Hello\" },\n      children: [],\n    });\n  });\n\n  it(\"handles flat spec parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"card-1\",\n            elements: {\n              \"card-1\": { type: \"Card\", props: {}, children: [] },\n            },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"card-1\");\n    expect(spec!.elements[\"card-1\"]).toBeDefined();\n  });\n\n  it(\"ignores non-spec parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Some text\" },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      { type: \"tool-invocation\", data: { toolName: \"search\" } },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBe(\"main\");\n  });\n\n  it(\"applies patches incrementally\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Stack\", props: {}, children: [\"child\"] },\n          },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/child\",\n            value: { type: \"Text\", props: { content: \"Hi\" }, children: [] },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(Object.keys(spec!.elements)).toHaveLength(2);\n    expect(spec!.elements.child!.props.content).toBe(\"Hi\");\n  });\n\n  it(\"handles nested spec parts via nestedToFlat\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"nested\",\n          spec: {\n            type: \"Card\",\n            props: { title: \"Nested\" },\n            children: [\n              { type: \"Text\", props: { content: \"Child\" }, children: [] },\n            ],\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    expect(spec!.root).toBeTruthy();\n    // nestedToFlat generates keys like el-0, el-1\n    const elementKeys = Object.keys(spec!.elements);\n    expect(elementKeys.length).toBe(2);\n\n    const rootEl = spec?.elements[spec.root];\n    expect(rootEl).toBeDefined();\n    expect(rootEl?.type).toBe(\"Card\");\n    expect(rootEl?.props.title).toBe(\"Nested\");\n    expect(rootEl?.children).toHaveLength(1);\n\n    const childKey = rootEl?.children?.[0];\n    const childEl = childKey ? spec?.elements[childKey] : undefined;\n    expect(childEl).toBeDefined();\n    expect(childEl?.type).toBe(\"Text\");\n    expect(childEl?.props.content).toBe(\"Child\");\n  });\n\n  it(\"handles mixed patch + flat + nested parts in sequence\", () => {\n    const parts = [\n      // Start with a patch\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        },\n      },\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Stack\", props: {}, children: [] },\n          },\n        },\n      },\n      // Then a flat spec overwrites everything\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"card-1\",\n            elements: {\n              \"card-1\": {\n                type: \"Card\",\n                props: { title: \"Flat\" },\n                children: [],\n              },\n            },\n          },\n        },\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n    expect(spec).not.toBeNull();\n    // Flat overwrites root and elements\n    expect(spec!.root).toBe(\"card-1\");\n    expect(spec!.elements[\"card-1\"]).toBeDefined();\n    expect(spec!.elements[\"card-1\"]!.type).toBe(\"Card\");\n  });\n\n  it(\"returns empty elements map from empty parts list\", () => {\n    const spec = buildSpecFromParts([]);\n    expect(spec).toBeNull();\n  });\n});\n\n// =============================================================================\n// getTextFromParts\n// =============================================================================\n\ndescribe(\"getTextFromParts\", () => {\n  it(\"extracts text from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"World\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"returns empty string when no text parts\", () => {\n    const parts = [\n      {\n        type: \"data-spec\",\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"x\" },\n        },\n      },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"\");\n  });\n\n  it(\"ignores non-text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Before\" },\n      { type: \"data-spec\", data: {} },\n      { type: \"tool-invocation\", data: {} },\n      { type: \"text\", text: \"After\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Before\\n\\nAfter\");\n  });\n\n  it(\"trims whitespace from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"  Hello  \" },\n      { type: \"text\", text: \"  World  \" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"skips empty text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"   \" },\n      { type: \"text\", text: \"World\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"ignores text parts with non-string text field\", () => {\n    const parts = [\n      { type: \"text\", text: \"Valid\" },\n      { type: \"text\", text: undefined as unknown as string },\n      { type: \"text\", text: 42 as unknown as string },\n      { type: \"text\", text: \"Also valid\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"Valid\\n\\nAlso valid\");\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/hooks.ts",
    "content": "import { createSignal, onCleanup, createMemo } from \"solid-js\";\nimport type {\n  Spec,\n  UIElement,\n  FlatElement,\n  JsonPatch,\n  SpecDataPart,\n} from \"@json-render/core\";\nimport {\n  setByPath,\n  getByPath,\n  addByPath,\n  removeByPath,\n  createMixedStreamParser,\n  applySpecPatch,\n  nestedToFlat,\n  SPEC_DATA_PART_TYPE,\n} from \"@json-render/core\";\n\n/**\n * Token usage metadata from AI generation\n */\nexport interface TokenUsage {\n  promptTokens: number;\n  completionTokens: number;\n  totalTokens: number;\n}\n\n/**\n * Parse result for a single line -- either a patch or usage metadata\n */\ntype ParsedLine =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"usage\"; usage: TokenUsage }\n  | null;\n\n/**\n * Parse a single JSON line (patch or metadata)\n */\nfunction parseLine(line: string): ParsedLine {\n  try {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"//\")) {\n      return null;\n    }\n    const parsed = JSON.parse(trimmed);\n\n    // Check for usage metadata\n    if (parsed.__meta === \"usage\") {\n      return {\n        type: \"usage\",\n        usage: {\n          promptTokens: parsed.promptTokens ?? 0,\n          completionTokens: parsed.completionTokens ?? 0,\n          totalTokens: parsed.totalTokens ?? 0,\n        },\n      };\n    }\n\n    return { type: \"patch\", patch: parsed as JsonPatch };\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Set a value at a spec path (for add/replace operations).\n */\nfunction setSpecValue(newSpec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    const statePath = path.slice(\"/state\".length); // e.g. \"/posts\"\n    setByPath(newSpec.state as Record<string, unknown>, statePath, value);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      newSpec.elements[elementKey] = value as UIElement;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Remove a value at a spec path.\n */\nfunction removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    delete newSpec.state;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    const statePath = path.slice(\"/state\".length);\n    removeByPath(newSpec.state as Record<string, unknown>, statePath);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      const { [elementKey]: _, ...rest } = newSpec.elements;\n      newSpec.elements = rest;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Get a value at a spec path.\n */\nfunction getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  if (path === \"/state\") return spec.state;\n  if (path.startsWith(\"/state/\") && spec.state) {\n    const statePath = path.slice(\"/state\".length);\n    return getByPath(spec.state as Record<string, unknown>, statePath);\n  }\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\n/**\n * Apply an RFC 6902 JSON patch to the current spec.\n * Supports add, remove, replace, move, copy, and test operations.\n */\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\": {\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    }\n    case \"remove\": {\n      removeSpecValue(newSpec, patch.path);\n      break;\n    }\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getSpecValue(newSpec, patch.from);\n      removeSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      // test is a no-op for rendering purposes (validation only)\n      break;\n    }\n  }\n\n  return newSpec;\n}\n\n/**\n * Options for useUIStream\n */\nexport interface UseUIStreamOptions {\n  /** API endpoint */\n  api: string;\n  /** Callback when complete */\n  onComplete?: (spec: Spec) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useUIStream\n */\nexport interface UseUIStreamReturn {\n  /** Current UI spec */\n  readonly spec: Spec | null;\n  /** Whether currently streaming */\n  readonly isStreaming: boolean;\n  /** Error if any */\n  readonly error: Error | null;\n  /** Token usage from the last generation */\n  readonly usage: TokenUsage | null;\n  /** Raw JSONL lines received from the stream (JSON patch lines) */\n  readonly rawLines: string[];\n  /** Send a prompt to generate UI */\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  /** Clear the current spec */\n  clear: () => void;\n}\n\n/**\n * Hook for streaming UI generation\n */\nexport function useUIStream(options: UseUIStreamOptions): UseUIStreamReturn {\n  const [spec, setSpec] = createSignal<Spec | null>(null);\n  const [isStreaming, setIsStreaming] = createSignal(false);\n  const [error, setError] = createSignal<Error | null>(null);\n  const [usage, setUsage] = createSignal<TokenUsage | null>(null);\n  const [rawLines, setRawLines] = createSignal<string[]>([]);\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    setSpec(null);\n    setError(null);\n  };\n\n  const send = async (prompt: string, context?: Record<string, unknown>) => {\n    // Abort any existing request\n    abortController?.abort();\n    abortController = new AbortController();\n\n    setIsStreaming(true);\n    setError(null);\n    setUsage(null);\n    setRawLines([]);\n\n    // Start with previous spec if provided, otherwise empty spec\n    const previousSpec = context?.previousSpec as Spec | undefined;\n    let currentSpec: Spec =\n      previousSpec && previousSpec.root\n        ? { ...previousSpec, elements: { ...previousSpec.elements } }\n        : { root: \"\", elements: {} };\n    setSpec(currentSpec);\n\n    try {\n      const response = await fetch(options.api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          prompt,\n          context,\n          currentSpec,\n        }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        // Try to parse JSON error response for better error messages\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) {\n            errorMessage = errorData.message;\n          } else if (errorData.error) {\n            errorMessage = errorData.error;\n          }\n        } catch {\n          // Ignore JSON parsing errors, use default message\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) {\n        throw new Error(\"No response body\");\n      }\n\n      const decoder = new TextDecoder();\n      let buffer = \"\";\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        buffer += decoder.decode(value, { stream: true });\n\n        // Process complete lines\n        const lines = buffer.split(\"\\n\");\n        buffer = lines.pop() ?? \"\";\n\n        for (const line of lines) {\n          const trimmed = line.trim();\n          if (!trimmed) continue;\n          const result = parseLine(trimmed);\n          if (!result) continue;\n          if (result.type === \"usage\") {\n            setUsage(result.usage);\n          } else {\n            setRawLines((prev) => [...prev, trimmed]);\n            currentSpec = applyPatch(currentSpec, result.patch);\n            setSpec({ ...currentSpec });\n          }\n        }\n      }\n\n      // Process any remaining buffer\n      if (buffer.trim()) {\n        const trimmed = buffer.trim();\n        const result = parseLine(trimmed);\n        if (result) {\n          if (result.type === \"usage\") {\n            setUsage(result.usage);\n          } else {\n            setRawLines((prev) => [...prev, trimmed]);\n            currentSpec = applyPatch(currentSpec, result.patch);\n            setSpec({ ...currentSpec });\n          }\n        }\n      }\n\n      options.onComplete?.(currentSpec);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") {\n        return;\n      }\n      const resolvedError = err instanceof Error ? err : new Error(String(err));\n      setError(resolvedError);\n      options.onError?.(resolvedError);\n    } finally {\n      setIsStreaming(false);\n    }\n  };\n\n  // Cleanup on disposal\n  onCleanup(() => {\n    abortController?.abort();\n  });\n\n  return {\n    get spec() {\n      return spec();\n    },\n    get isStreaming() {\n      return isStreaming();\n    },\n    get error() {\n      return error();\n    },\n    get usage() {\n      return usage();\n    },\n    get rawLines() {\n      return rawLines();\n    },\n    send,\n    clear,\n  };\n}\n\n/**\n * Convert a flat element list to a Spec.\n * Input elements use key/parentKey to establish identity and relationships.\n * Output spec uses the map-based format where key is the map entry key\n * and parent-child relationships are expressed through children arrays.\n */\nexport function flatToTree(elements: FlatElement[]): Spec {\n  const elementMap: Record<string, UIElement> = {};\n  let root = \"\";\n\n  // First pass: add all elements to map\n  for (const element of elements) {\n    elementMap[element.key] = {\n      type: element.type,\n      props: element.props,\n      children: [],\n      visible: element.visible,\n    };\n  }\n\n  // Second pass: build parent-child relationships\n  for (const element of elements) {\n    if (element.parentKey) {\n      const parent = elementMap[element.parentKey];\n      if (parent) {\n        if (!parent.children) {\n          parent.children = [];\n        }\n        parent.children.push(element.key);\n      }\n    } else {\n      root = element.key;\n    }\n  }\n\n  return { root, elements: elementMap };\n}\n\n// =============================================================================\n// useBoundProp — Two-way binding helper for $bindState/$bindItem expressions\n// =============================================================================\n\n// Re-export useStateStore access for useBoundProp without circular import\nimport { useStateStore as useStateStoreFromContext } from \"./contexts/state\";\n\n/**\n * Hook for two-way bound props. Returns `[value, setValue]` where:\n *\n * - `value` is the already-resolved prop value (passed through from render props)\n * - `setValue` writes back to the bound state path (no-op if not bound)\n *\n * Designed to work with the `bindings` map that the renderer provides when\n * a prop uses `{ $bindState: \"/path\" }` or `{ $bindItem: \"field\" }`.\n *\n * @example\n * ```tsx\n * import { useBoundProp } from \"@json-render/solid\";\n *\n * const Input: ComponentRenderer = (ctx) => {\n *   const [value, setValue] = useBoundProp<string>(ctx.props.value, ctx.bindings?.value);\n *   return <input value={value ?? \"\"} onInput={(e) => setValue(e.target.value)} />;\n * };\n * ```\n */\nexport function useBoundProp<T>(\n  propValue: T | undefined,\n  bindingPath: string | undefined,\n): [T | undefined, (value: T) => void] {\n  const { set } = useStateStoreFromContext();\n  const setValue = (value: T) => {\n    if (bindingPath) set(bindingPath, value);\n  };\n  return [propValue, setValue];\n}\n\n// =============================================================================\n// buildSpecFromParts — Derive Spec from AI SDK data parts\n// =============================================================================\n\n/**\n * A single part from the AI SDK's `message.parts` array. This is a minimal\n * structural type so that library helpers do not depend on the AI SDK.\n * Fields are optional because different part types carry different data:\n * - Text parts have `text`\n * - Data parts have `data`\n */\nexport interface DataPart {\n  type: string;\n  text?: string;\n  data?: unknown;\n}\n\n/**\n * Build a `Spec` by replaying all spec data parts from a message's\n * parts array. Returns `null` if no spec data parts are present.\n *\n * This function is designed to work with the AI SDK's `UIMessage.parts` array.\n * It picks out parts whose `type` is {@link SPEC_DATA_PART_TYPE} and processes them based\n * on the payload's `type` discriminator:\n *\n * - `\"patch\"`: Applies the JSON Patch operation incrementally via `applySpecPatch`.\n * - `\"flat\"`: Replaces the spec with the complete flat spec.\n * - `\"nested\"`: Assigns the nested spec directly (future: nested-to-flat conversion).\n *\n * The function has no AI SDK dependency -- it operates on a generic array of\n * `{ type: string; data: unknown }` objects.\n *\n * @example\n * ```tsx\n * const spec = buildSpecFromParts(message.parts);\n * if (spec) {\n *   return <MyRenderer spec={spec} />;\n * }\n * ```\n */\n/**\n * Type guard that validates a data part payload looks like a valid\n * {@link SpecDataPart} before we cast it. Returns `false` (and the\n * part is silently skipped) for malformed payloads.\n */\nfunction isSpecDataPart(data: unknown): data is SpecDataPart {\n  if (typeof data !== \"object\" || data === null) return false;\n  const obj = data as Record<string, unknown>;\n  switch (obj.type) {\n    case \"patch\":\n      return typeof obj.patch === \"object\" && obj.patch !== null;\n    case \"flat\":\n    case \"nested\":\n      return typeof obj.spec === \"object\" && obj.spec !== null;\n    default:\n      return false;\n  }\n}\n\nexport function buildSpecFromParts(parts: DataPart[]): Spec | null {\n  const spec: Spec = { root: \"\", elements: {} };\n  let hasSpec = false;\n\n  for (const part of parts) {\n    if (part.type === SPEC_DATA_PART_TYPE) {\n      if (!isSpecDataPart(part.data)) continue;\n      const payload = part.data;\n      if (payload.type === \"patch\") {\n        hasSpec = true;\n        applySpecPatch(spec, payload.patch);\n      } else if (payload.type === \"flat\") {\n        hasSpec = true;\n        Object.assign(spec, payload.spec);\n      } else if (payload.type === \"nested\") {\n        hasSpec = true;\n        const flat = nestedToFlat(payload.spec);\n        Object.assign(spec, flat);\n      }\n    }\n  }\n\n  return hasSpec ? spec : null;\n}\n\n/**\n * Extract and join all text content from a message's parts array.\n *\n * Filters for parts with `type === \"text\"`, trims each one, and joins them\n * with double newlines so that text from separate agent steps renders as\n * distinct paragraphs in markdown.\n *\n * Has no AI SDK dependency -- operates on a generic `DataPart[]`.\n *\n * @example\n * ```tsx\n * const text = getTextFromParts(message.parts);\n * if (text) {\n *   return <Streamdown>{text}</Streamdown>;\n * }\n * ```\n */\nexport function getTextFromParts(parts: DataPart[]): string {\n  return parts\n    .filter(\n      (p): p is DataPart & { text: string } =>\n        p.type === \"text\" && typeof p.text === \"string\",\n    )\n    .map((p) => p.text.trim())\n    .filter(Boolean)\n    .join(\"\\n\\n\");\n}\n\n// =============================================================================\n// useJsonRenderMessage — extract spec + text from message parts\n// =============================================================================\n\n/**\n * Hook that extracts both the json-render spec and text content from a\n * message's parts array. Combines `buildSpecFromParts` and `getTextFromParts`\n * into a single call with memoized results.\n *\n * **Memoization behavior:** Results are recomputed only when the `parts`\n * accessor returns a new array reference with a different length or last\n * element. This is optimized for the typical AI SDK streaming pattern where\n * parts are appended incrementally.\n *\n * @example\n * ```tsx\n * import { useJsonRenderMessage } from \"@json-render/solid\";\n *\n * function MessageBubble(props: { parts: DataPart[] }) {\n *   const msg = useJsonRenderMessage(() => props.parts);\n *\n *   return (\n *     <div>\n *       {msg.text && <Markdown>{msg.text}</Markdown>}\n *       {msg.hasSpec && <MyRenderer spec={msg.spec!} />}\n *     </div>\n *   );\n * }\n * ```\n */\nexport function useJsonRenderMessage(getParts: () => DataPart[]) {\n  const result = createMemo(() => {\n    const parts = getParts();\n    return {\n      spec: buildSpecFromParts(parts),\n      text: getTextFromParts(parts),\n    };\n  });\n\n  return {\n    get spec() {\n      return result().spec;\n    },\n    get text() {\n      return result().text;\n    },\n    get hasSpec() {\n      const s = result().spec;\n      return s !== null && Object.keys(s.elements || {}).length > 0;\n    },\n  };\n}\n\n// =============================================================================\n// useChatUI — Chat + GenUI hook\n// =============================================================================\n\n/**\n * A single message in the chat, which may contain text, a rendered UI spec, or both.\n */\nexport interface ChatMessage {\n  /** Unique message ID */\n  id: string;\n  /** Who sent this message */\n  role: \"user\" | \"assistant\";\n  /** Text content (conversational prose) */\n  text: string;\n  /** json-render Spec built from JSONL patches (null if no UI was generated) */\n  spec: Spec | null;\n}\n\n/**\n * Options for useChatUI\n */\nexport interface UseChatUIOptions {\n  /** API endpoint that accepts `{ messages: Array<{ role, content }> }` and returns a text stream */\n  api: string;\n  /** Callback when streaming completes for a message */\n  onComplete?: (message: ChatMessage) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useChatUI\n */\nexport interface UseChatUIReturn {\n  /** All messages in the conversation */\n  readonly messages: ChatMessage[];\n  /** Whether currently streaming an assistant response */\n  readonly isStreaming: boolean;\n  /** Error from the last request, if any */\n  readonly error: Error | null;\n  /** Send a user message */\n  send: (text: string) => Promise<void>;\n  /** Clear all messages and reset the conversation */\n  clear: () => void;\n}\n\nlet chatMessageIdCounter = 0;\nfunction generateChatId(): string {\n  if (\n    typeof crypto !== \"undefined\" &&\n    typeof crypto.randomUUID === \"function\"\n  ) {\n    return crypto.randomUUID();\n  }\n  chatMessageIdCounter += 1;\n  return `msg-${Date.now()}-${chatMessageIdCounter}`;\n}\n\n/**\n * Hook for chat + GenUI experiences.\n *\n * Manages a multi-turn conversation where each assistant message can contain\n * both conversational text and a json-render UI spec. The hook sends the full\n * message history to the API endpoint, reads the streamed response, and\n * separates text lines from JSONL patch lines using `createMixedStreamParser`.\n *\n * @example\n * ```tsx\n * const chat = useChatUI({ api: \"/api/chat\" });\n *\n * // Send a message\n * await chat.send(\"Compare weather in NYC and Tokyo\");\n *\n * // Render messages\n * <For each={chat.messages}>\n *   {(msg) => (\n *     <div>\n *       {msg.text && <p>{msg.text}</p>}\n *       {msg.spec && <MyRenderer spec={msg.spec} />}\n *     </div>\n *   )}\n * </For>\n * ```\n */\nexport function useChatUI(options: UseChatUIOptions): UseChatUIReturn {\n  const [messages, setMessages] = createSignal<ChatMessage[]>([]);\n  const [isStreaming, setIsStreaming] = createSignal(false);\n  const [error, setError] = createSignal<Error | null>(null);\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    setMessages([]);\n    setError(null);\n  };\n\n  const send = async (text: string) => {\n    if (!text.trim()) return;\n\n    // Abort any existing request\n    abortController?.abort();\n    abortController = new AbortController();\n\n    const userMessage: ChatMessage = {\n      id: generateChatId(),\n      role: \"user\",\n      text: text.trim(),\n      spec: null,\n    };\n\n    const assistantId = generateChatId();\n    const assistantMessage: ChatMessage = {\n      id: assistantId,\n      role: \"assistant\",\n      text: \"\",\n      spec: null,\n    };\n\n    // Append user message and empty assistant placeholder\n    setMessages((prev) => [...prev, userMessage, assistantMessage]);\n    setIsStreaming(true);\n    setError(null);\n\n    // Build messages array for the API (full conversation history + new message).\n    // Read current messages signal for latest state (avoids stale closure).\n    const historyForApi = [\n      ...messages()\n        .filter((m) => m.id !== userMessage.id && m.id !== assistantId)\n        .map((m) => ({\n          role: m.role,\n          content: m.text,\n        })),\n      { role: \"user\" as const, content: text.trim() },\n    ];\n\n    // Mutable state for accumulating the assistant response\n    let accumulatedText = \"\";\n    let currentSpec: Spec = { root: \"\", elements: {} };\n    let hasSpec = false;\n\n    try {\n      const response = await fetch(options.api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ messages: historyForApi }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) {\n            errorMessage = errorData.message;\n          } else if (errorData.error) {\n            errorMessage = errorData.error;\n          }\n        } catch {\n          // Ignore JSON parsing errors\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) {\n        throw new Error(\"No response body\");\n      }\n\n      const decoder = new TextDecoder();\n\n      // Use createMixedStreamParser to classify lines\n      const parser = createMixedStreamParser({\n        onPatch(patch) {\n          hasSpec = true;\n          applySpecPatch(currentSpec, patch);\n          setMessages((prev) =>\n            prev.map((m) =>\n              m.id === assistantId\n                ? {\n                    ...m,\n                    spec: {\n                      root: currentSpec.root,\n                      elements: { ...currentSpec.elements },\n                      ...(currentSpec.state\n                        ? { state: { ...currentSpec.state } }\n                        : {}),\n                    },\n                  }\n                : m,\n            ),\n          );\n        },\n        onText(line) {\n          accumulatedText += (accumulatedText ? \"\\n\" : \"\") + line;\n          setMessages((prev) =>\n            prev.map((m) =>\n              m.id === assistantId ? { ...m, text: accumulatedText } : m,\n            ),\n          );\n        },\n      });\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        parser.push(decoder.decode(value, { stream: true }));\n      }\n      parser.flush();\n\n      // Build final message for onComplete callback\n      const finalMessage: ChatMessage = {\n        id: assistantId,\n        role: \"assistant\",\n        text: accumulatedText,\n        spec: hasSpec\n          ? {\n              root: currentSpec.root,\n              elements: { ...currentSpec.elements },\n              ...(currentSpec.state ? { state: { ...currentSpec.state } } : {}),\n            }\n          : null,\n      };\n      options.onComplete?.(finalMessage);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") {\n        return;\n      }\n      const resolvedError = err instanceof Error ? err : new Error(String(err));\n      setError(resolvedError);\n      // Remove empty assistant message on error\n      setMessages((prev) =>\n        prev.filter((m) => m.id !== assistantId || m.text.length > 0),\n      );\n      options.onError?.(resolvedError);\n    } finally {\n      setIsStreaming(false);\n    }\n  };\n\n  // Cleanup on disposal\n  onCleanup(() => {\n    abortController?.abort();\n  });\n\n  return {\n    get messages() {\n      return messages();\n    },\n    get isStreaming() {\n      return isStreaming();\n    },\n    get error() {\n      return error();\n    },\n    send,\n    clear,\n  };\n}\n"
  },
  {
    "path": "packages/solid/src/index.ts",
    "content": "// Contexts\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./contexts/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n  type VisibilityProviderProps,\n} from \"./contexts/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./contexts/actions\";\n\nexport {\n  ValidationProvider,\n  useValidation,\n  useOptionalValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./contexts/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/repeat-scope\";\n\n// Schema (Solid's spec format)\nexport {\n  schema,\n  type SolidSchema,\n  type SolidSpec,\n  // Backward compatibility\n  elementTreeSchema,\n  type ElementTreeSchema,\n  type ElementTreeSpec,\n} from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec, StateStore } from \"@json-render/core\";\nexport { createStateStore } from \"@json-render/core\";\n\n// Catalog-aware types for Solid\nexport type {\n  EventHandle,\n  BaseComponentProps,\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n  ActionFn,\n  Actions,\n} from \"./catalog-types\";\n\n// Renderer\nexport {\n  // Registry\n  defineRegistry,\n  type DefineRegistryResult,\n  // createRenderer (higher-level, includes providers)\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  // Low-level\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRenderer,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n} from \"./renderer\";\n\n// Hooks\nexport {\n  useUIStream,\n  useChatUI,\n  useBoundProp,\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n  useJsonRenderMessage,\n  type UseUIStreamOptions,\n  type UseUIStreamReturn,\n  type UseChatUIOptions,\n  type UseChatUIReturn,\n  type ChatMessage,\n  type DataPart,\n  type TokenUsage,\n} from \"./hooks\";\n"
  },
  {
    "path": "packages/solid/src/renderer.test.tsx",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { Renderer } from \"./renderer\";\n\ndescribe(\"Renderer\", () => {\n  it(\"is a valid component function\", () => {\n    expect(typeof Renderer).toBe(\"function\");\n  });\n\n  it(\"accepts null spec\", () => {\n    const props = {\n      spec: null,\n      registry: {},\n    };\n    expect(props.spec).toBeNull();\n    expect(props.registry).toEqual({});\n  });\n\n  it(\"accepts spec without root\", () => {\n    const props = {\n      spec: { root: \"\", elements: {} },\n      registry: {},\n    };\n    expect(props.spec.root).toBe(\"\");\n    expect(props.spec.elements).toEqual({});\n  });\n\n  it(\"accepts loading prop\", () => {\n    const props = {\n      spec: null,\n      registry: {},\n      loading: true,\n    };\n    expect(props.loading).toBe(true);\n  });\n\n  it(\"accepts fallback prop\", () => {\n    const Fallback = () => {\n      const el = document.createElement(\"div\");\n      el.textContent = \"Unknown component\";\n      return el;\n    };\n\n    const props = {\n      spec: null,\n      registry: {},\n      fallback: Fallback,\n    };\n    expect(props.fallback).toBe(Fallback);\n  });\n});\n"
  },
  {
    "path": "packages/solid/src/renderer.tsx",
    "content": "import {\n  createContext,\n  useContext,\n  createMemo,\n  createEffect,\n  onCleanup,\n  ErrorBoundary,\n  Show,\n  For,\n  type JSX,\n  type Component,\n} from \"solid-js\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  SchemaDefinition,\n  StateStore,\n  ComputedFunction,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport type {\n  Components,\n  Actions,\n  ActionFn,\n  SetState,\n  StateModel,\n  CatalogHasActions,\n  EventHandle,\n} from \"./catalog-types\";\nimport { useIsVisible, useVisibility } from \"./contexts/visibility\";\nimport { useActions } from \"./contexts/actions\";\nimport { useStateStore } from \"./contexts/state\";\nimport { StateProvider } from \"./contexts/state\";\nimport { VisibilityProvider } from \"./contexts/visibility\";\nimport { ActionProvider } from \"./contexts/actions\";\nimport { ValidationProvider } from \"./contexts/validation\";\nimport { ConfirmDialog } from \"./contexts/actions\";\nimport { RepeatScopeProvider, useRepeatScope } from \"./contexts/repeat-scope\";\n\n/**\n * Props passed to component renderers\n */\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  /** The element being rendered */\n  element: UIElement<string, P>;\n  /** Rendered children */\n  children?: JSX.Element;\n  /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. Always provided by the renderer. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata (shouldPreventDefault, bound). Use when you need to inspect event bindings. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   * Only present when at least one prop uses `{ $bindState: \"...\" }` or `{ $bindItem: \"...\" }`.\n   */\n  bindings?: Record<string, string>;\n  /** Whether the parent is loading */\n  loading?: boolean;\n}\n\n/**\n * Component renderer type\n */\nexport type ComponentRenderer<P = Record<string, unknown>> = Component<\n  ComponentRenderProps<P>\n>;\n\n/**\n * Registry of component renderers\n */\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\n/**\n * Props for the Renderer component\n */\nexport interface RendererProps {\n  /** The UI spec to render */\n  spec: Spec | null;\n  /** Component registry */\n  registry: ComponentRegistry;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n// ---------------------------------------------------------------------------\n// FunctionsContext – provides $computed functions to the element tree\n// ---------------------------------------------------------------------------\n\nconst EMPTY_FUNCTIONS: Record<string, ComputedFunction> = {};\n\nconst FunctionsContext =\n  createContext<Record<string, ComputedFunction>>(EMPTY_FUNCTIONS);\n\nfunction useFunctions(): Record<string, ComputedFunction> {\n  return useContext(FunctionsContext) ?? EMPTY_FUNCTIONS;\n}\n\ninterface ElementRendererProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Element renderer component.\n */\nfunction ElementRenderer(props: ElementRendererProps) {\n  const repeatScope = useRepeatScope();\n  const { ctx } = useVisibility();\n  const { execute } = useActions();\n  const stateStore = useStateStore();\n  const getSnapshot = () => stateStore.getSnapshot();\n  const functions = useFunctions();\n\n  // Build context with repeat scope and $computed functions\n  const fullCtx = createMemo<PropResolutionContext>(() => {\n    const repeatItem = repeatScope?.item;\n    const repeatIndex = repeatScope?.index;\n    const repeatBasePath = repeatScope?.basePath;\n\n    return {\n      get stateModel() {\n        return ctx.stateModel;\n      },\n      ...(repeatItem !== undefined ? { repeatItem } : {}),\n      ...(repeatIndex !== undefined ? { repeatIndex } : {}),\n      ...(repeatBasePath !== undefined ? { repeatBasePath } : {}),\n      functions,\n    };\n  });\n\n  // Evaluate visibility (now supports $item/$index inside repeat scopes)\n  const isVisible = createMemo(() =>\n    props.element.visible === undefined\n      ? true\n      : evaluateVisibility(props.element.visible, fullCtx()),\n  );\n\n  // Create emit function that resolves events to action bindings.\n  const emit = async (eventName: string) => {\n    const onBindings = props.element.on;\n    const binding = onBindings?.[eventName];\n    if (!binding) return;\n    const actionBindings = Array.isArray(binding) ? binding : [binding];\n    for (const b of actionBindings) {\n      if (!b.params) {\n        await execute(b);\n        continue;\n      }\n      // Build a fresh context with live store state so that $state\n      // references in later actions see mutations from earlier ones.\n      const liveCtx: PropResolutionContext = {\n        ...fullCtx(),\n        stateModel: getSnapshot(),\n      };\n      const resolved: Record<string, unknown> = {};\n      for (const [key, val] of Object.entries(b.params)) {\n        resolved[key] = resolveActionParam(val, liveCtx);\n      }\n      await execute({ ...b, params: resolved });\n    }\n  };\n\n  // Create on() function that returns an EventHandle with metadata for a specific event.\n  const on = (eventName: string): EventHandle => {\n    const onBindings = props.element.on;\n    const binding = onBindings?.[eventName];\n    if (!binding) {\n      return { emit: () => {}, shouldPreventDefault: false, bound: false };\n    }\n    const actionBindings = Array.isArray(binding) ? binding : [binding];\n    const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);\n    return {\n      emit: () => emit(eventName),\n      shouldPreventDefault,\n      bound: true,\n    };\n  };\n\n  // Watch effect: fire actions when watched state paths change.\n  createEffect(() => {\n    const watchConfig = props.element.watch;\n    if (!watchConfig) return;\n    const paths = Object.keys(watchConfig);\n    if (paths.length === 0) return;\n\n    const unsubscribe = stateStore.subscribeChanges((changes) => {\n      const changedPaths = new Set(changes.map((change) => change.path));\n\n      void (async () => {\n        for (const path of paths) {\n          if (!changedPaths.has(path)) continue;\n\n          const binding = watchConfig[path];\n          if (!binding) continue;\n          const bindings = Array.isArray(binding) ? binding : [binding];\n\n          for (const b of bindings) {\n            if (!b.params) {\n              await execute(b);\n              continue;\n            }\n            const liveCtx: PropResolutionContext = {\n              ...fullCtx(),\n              stateModel: getSnapshot(),\n            };\n            const resolved: Record<string, unknown> = {};\n            for (const [key, val] of Object.entries(b.params)) {\n              resolved[key] = resolveActionParam(val, liveCtx);\n            }\n            await execute({ ...b, params: resolved });\n          }\n        }\n      })().catch(console.error);\n    });\n\n    onCleanup(() => {\n      unsubscribe();\n    });\n  });\n\n  // Resolve $bindState/$bindItem expressions → bindings map (prop name → state path)\n  const elementBindings = createMemo(() => {\n    const rawProps = props.element.props as Record<string, unknown>;\n    return resolveBindings(rawProps, fullCtx());\n  });\n\n  // Resolve dynamic prop expressions ($state, $item, $index, $bindState, $bindItem, $cond/$then/$else)\n  const resolvedElement = createMemo(() => {\n    const rawProps = props.element.props as Record<string, unknown>;\n    const resolvedProps = resolveElementProps(rawProps, fullCtx());\n    return resolvedProps !== props.element.props\n      ? { ...props.element, props: resolvedProps }\n      : props.element;\n  });\n\n  return (\n    <Show when={isVisible()}>\n      <ErrorBoundary\n        fallback={(err) => {\n          console.error(\n            `[json-render] Rendering error in <${props.element.type}>:`,\n            err,\n          );\n          return null;\n        }}\n      >\n        <ElementRendererContent\n          resolvedElement={resolvedElement()}\n          spec={props.spec}\n          registry={props.registry}\n          loading={props.loading}\n          fallback={props.fallback}\n          emit={emit}\n          on={on}\n          bindings={elementBindings()}\n        />\n      </ErrorBoundary>\n    </Show>\n  );\n}\n\ninterface ElementRendererContentProps {\n  resolvedElement: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n}\n\n/**\n * Inner content renderer, separated so it can be wrapped in Show + ErrorBoundary.\n */\nfunction ElementRendererContent(props: ElementRendererContentProps) {\n  // Get the component renderer\n  const Comp = () =>\n    props.registry[props.resolvedElement.type] ?? props.fallback;\n\n  return (\n    <Show\n      when={Comp()}\n      fallback={(() => {\n        console.warn(\n          `No renderer for component type: ${props.resolvedElement.type}`,\n        );\n        return null;\n      })()}\n    >\n      {(ComponentFn) => {\n        const Component = ComponentFn();\n        // Render children (with repeat support)\n        const children = () =>\n          props.resolvedElement.repeat ? (\n            <RepeatChildren\n              element={props.resolvedElement}\n              spec={props.spec}\n              registry={props.registry}\n              loading={props.loading}\n              fallback={props.fallback}\n            />\n          ) : (\n            <For each={props.resolvedElement.children ?? []}>\n              {(childKey) => {\n                const childElement = () => props.spec.elements[childKey];\n                return (\n                  <Show\n                    when={childElement()}\n                    fallback={(() => {\n                      if (!props.loading) {\n                        console.warn(\n                          `[json-render] Missing element \"${childKey}\" referenced as child of \"${props.resolvedElement.type}\". This element will not render.`,\n                        );\n                      }\n                      return null;\n                    })()}\n                  >\n                    {(el) => (\n                      <ElementRenderer\n                        element={el()}\n                        spec={props.spec}\n                        registry={props.registry}\n                        loading={props.loading}\n                        fallback={props.fallback}\n                      />\n                    )}\n                  </Show>\n                );\n              }}\n            </For>\n          );\n\n        return (\n          <Component\n            element={props.resolvedElement}\n            emit={props.emit}\n            on={props.on}\n            bindings={props.bindings}\n            loading={props.loading}\n          >\n            {children()}\n          </Component>\n        );\n      }}\n    </Show>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// RepeatChildren -- renders child elements once per item in a state array.\n// Used when an element has a `repeat` field.\n// ---------------------------------------------------------------------------\n\ninterface RepeatChildrenProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: ComponentRenderer;\n}\n\nfunction RepeatChildren(props: RepeatChildrenProps) {\n  const stateStore = useStateStore();\n  const repeat = () => props.element.repeat!;\n  const statePath = () => repeat().statePath;\n\n  const items = () =>\n    (getByPath(stateStore.state, statePath()) as unknown[] | undefined) ?? [];\n\n  return (\n    <For each={items()}>\n      {(itemValue, index) => {\n        const key = () => {\n          const r = repeat();\n          return r.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String((itemValue as Record<string, unknown>)[r.key] ?? index())\n            : String(index());\n        };\n\n        return (\n          <RepeatScopeProvider\n            item={itemValue}\n            index={index()}\n            basePath={`${statePath()}/${index()}`}\n          >\n            <For each={props.element.children ?? []}>\n              {(childKey) => {\n                const childElement = () => props.spec.elements[childKey];\n                return (\n                  <Show\n                    when={childElement()}\n                    fallback={(() => {\n                      if (!props.loading) {\n                        console.warn(\n                          `[json-render] Missing element \"${childKey}\" referenced as child of \"${props.element.type}\" (repeat). This element will not render.`,\n                        );\n                      }\n                      return null;\n                    })()}\n                  >\n                    {(el) => (\n                      <ElementRenderer\n                        element={el()}\n                        spec={props.spec}\n                        registry={props.registry}\n                        loading={props.loading}\n                        fallback={props.fallback}\n                      />\n                    )}\n                  </Show>\n                );\n              }}\n            </For>\n          </RepeatScopeProvider>\n        );\n      }}\n    </For>\n  );\n}\n\n/**\n * Main renderer component\n */\nexport function Renderer(props: RendererProps) {\n  const rootElement = createMemo(() => {\n    const spec = props.spec;\n    if (!spec || !spec.root) return undefined;\n    return spec.elements[spec.root];\n  });\n\n  return (\n    <Show when={rootElement()}>\n      {(el) => (\n        <ElementRenderer\n          element={el()}\n          spec={props.spec!}\n          registry={props.registry}\n          loading={props.loading}\n          fallback={props.fallback}\n        />\n      )}\n    </Show>\n  );\n}\n\n/**\n * Props for JSONUIProvider\n */\nexport interface JSONUIProviderProps {\n  /** Component registry */\n  registry: ComponentRegistry;\n  /**\n   * External store (controlled mode). When provided, `initialState` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** Initial state model (uncontrolled mode) */\n  initialState?: Record<string, unknown>;\n  /** Action handlers */\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  /** Navigation function */\n  navigate?: (path: string) => void;\n  /** Custom validation functions */\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  children: JSX.Element;\n}\n\n/**\n * Combined provider for all JSONUI contexts\n */\nexport function JSONUIProvider(props: JSONUIProviderProps) {\n  return (\n    <StateProvider\n      store={props.store}\n      initialState={props.initialState}\n      onStateChange={props.onStateChange}\n    >\n      <VisibilityProvider>\n        <ValidationProvider customFunctions={props.validationFunctions}>\n          <ActionProvider handlers={props.handlers} navigate={props.navigate}>\n            <FunctionsContext.Provider\n              value={props.functions ?? EMPTY_FUNCTIONS}\n            >\n              {props.children}\n              <ConfirmationDialogManager />\n            </FunctionsContext.Provider>\n          </ActionProvider>\n        </ValidationProvider>\n      </VisibilityProvider>\n    </StateProvider>\n  );\n}\n\n/**\n * Renders the confirmation dialog when needed\n */\nfunction ConfirmationDialogManager() {\n  const { pendingConfirmation, confirm, cancel } = useActions();\n\n  return (\n    <Show when={pendingConfirmation?.action.confirm}>\n      {(confirmConfig) => (\n        <ConfirmDialog\n          confirm={confirmConfig()}\n          onConfirm={confirm}\n          onCancel={cancel}\n        />\n      )}\n    </Show>\n  );\n}\n\n// ============================================================================\n// defineRegistry\n// ============================================================================\n\n/**\n * Result returned by defineRegistry\n */\nexport interface DefineRegistryResult {\n  /** Component registry for `<Renderer registry={...} />` */\n  registry: ComponentRegistry;\n  /**\n   * Create ActionProvider-compatible handlers.\n   * Accepts getter functions so handlers always read the latest state/setState\n   * (e.g. from refs or closures).\n   */\n  handlers: (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;\n  /**\n   * Execute an action by name imperatively\n   * (for use outside the Solid tree, e.g. initial state loading).\n   */\n  executeAction: (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state?: StateModel,\n  ) => Promise<void>;\n}\n\n/**\n * Options for defineRegistry.\n *\n * When the catalog declares actions, the `actions` field is required.\n * When the catalog has no actions (or `actions: {}`), the field is optional.\n */\ntype DefineRegistryOptions<C extends Catalog> = {\n  components?: Components<C>;\n} & (CatalogHasActions<C> extends true\n  ? { actions: Actions<C> }\n  : { actions?: Actions<C> });\n\n/**\n * Create a registry from a catalog with components and/or actions.\n *\n * When the catalog declares actions, the `actions` field is required.\n *\n * @example\n * ```tsx\n * // Components only (catalog has no actions)\n * const { registry } = defineRegistry(catalog, {\n *   components: {\n *     Card: (ctx) => (\n *       <div class=\"card\">{ctx.props.title}{ctx.children}</div>\n *     ),\n *   },\n * });\n *\n * // Both (catalog declares actions)\n * const { registry, handlers, executeAction } = defineRegistry(catalog, {\n *   components: { ... },\n *   actions: { ... },\n * });\n * ```\n */\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: DefineRegistryOptions<C>,\n): DefineRegistryResult {\n  // Build component registry\n  const registry: ComponentRegistry = {};\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = (renderProps: ComponentRenderProps) => {\n        return (componentFn as DefineRegistryComponentFn)({\n          get props() {\n            return renderProps.element.props;\n          },\n          get children() {\n            return renderProps.children;\n          },\n          emit: renderProps.emit,\n          on: renderProps.on,\n          get bindings() {\n            return renderProps.bindings;\n          },\n          get loading() {\n            return renderProps.loading;\n          },\n        });\n      };\n    }\n  }\n\n  // Build action helpers\n  const actionMap = options.actions\n    ? (Object.entries(options.actions) as Array<\n        [string, DefineRegistryActionFn]\n      >)\n    : [];\n\n  const handlers = (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {\n    const result: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void>\n    > = {};\n    for (const [name, actionFn] of actionMap) {\n      result[name] = async (params) => {\n        const setState = getSetState();\n        const state = getState();\n        if (setState) {\n          await actionFn(params, setState, state);\n        }\n      };\n    }\n    return result;\n  };\n\n  const executeAction = async (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state: StateModel = {},\n  ): Promise<void> => {\n    const entry = actionMap.find(([name]) => name === actionName);\n    if (entry) {\n      await entry[1](params, setState, state);\n    } else {\n      console.warn(`Unknown action: ${actionName}`);\n    }\n  };\n\n  return { registry, handlers, executeAction };\n}\n\n/** @internal */\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: JSX.Element;\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => JSX.Element;\n\n/** @internal */\ntype DefineRegistryActionFn = (\n  params: Record<string, unknown> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n// ============================================================================\n// NEW API\n// ============================================================================\n\n/**\n * Props for renderers created with createRenderer\n */\nexport interface CreateRendererProps {\n  /** The spec to render (AI-generated JSON) */\n  spec: Spec | null;\n  /**\n   * External store (controlled mode). When provided, `state` and\n   * `onStateChange` are ignored.\n   */\n  store?: StateStore;\n  /** State context for dynamic values (uncontrolled mode) */\n  state?: Record<string, unknown>;\n  /** Action handler */\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  /** Callback when state changes (uncontrolled mode) */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  /** Whether the spec is currently loading/streaming */\n  loading?: boolean;\n  /** Fallback component for unknown types */\n  fallback?: ComponentRenderer;\n}\n\n/**\n * Component map type - maps component names to Solid components\n */\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: Component<\n    ComponentRenderProps<\n      TComponents[K][\"props\"] extends { _output: infer O }\n        ? O\n        : Record<string, unknown>\n    >\n  >;\n};\n\n/**\n * Create a renderer from a catalog\n *\n * @example\n * ```typescript\n * const DashboardRenderer = createRenderer(dashboardCatalog, {\n *   Card: (renderProps) => <div class=\"card\">{renderProps.children}</div>,\n *   Metric: (renderProps) => <span>{renderProps.element.props.value}</span>,\n * });\n *\n * // Usage\n * <DashboardRenderer spec={aiGeneratedSpec} state={state} />\n * ```\n */\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): Component<CreateRendererProps> {\n  // Convert component map to registry\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  // Return the renderer component\n  return function CatalogRenderer(props: CreateRendererProps) {\n    // Wrap onAction with a Proxy so any action name routes to the callback\n    const actionHandlers = () =>\n      props.onAction\n        ? new Proxy(\n            {} as Record<\n              string,\n              (params: Record<string, unknown>) => void | Promise<void>\n            >,\n            {\n              get: (_target, prop: string) => {\n                return (params: Record<string, unknown>) =>\n                  props.onAction!(prop, params);\n              },\n              has: () => true,\n            },\n          )\n        : undefined;\n\n    return (\n      <StateProvider\n        store={props.store}\n        initialState={props.state}\n        onStateChange={props.onStateChange}\n      >\n        <VisibilityProvider>\n          <ValidationProvider>\n            <ActionProvider handlers={actionHandlers()}>\n              <FunctionsContext.Provider\n                value={props.functions ?? EMPTY_FUNCTIONS}\n              >\n                <Renderer\n                  spec={props.spec}\n                  registry={registry}\n                  loading={props.loading}\n                  fallback={props.fallback}\n                />\n                <ConfirmationDialogManager />\n              </FunctionsContext.Provider>\n            </ActionProvider>\n          </ValidationProvider>\n        </VisibilityProvider>\n      </StateProvider>\n    );\n  };\n}\n"
  },
  {
    "path": "packages/solid/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/solid\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like\n    spec: s.object({\n      /** Root element key */\n      root: s.string(),\n      /** Flat map of elements by key */\n      elements: s.record(\n        s.object({\n          /** Component type from catalog */\n          type: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Child element keys (flat reference) */\n          children: s.array(s.string()),\n          /** Visibility condition */\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Component definitions */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n        slots: s.array(s.string()),\n        /** Description for AI generation hints */\n        description: s.string(),\n        /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n        example: s.any(),\n      }),\n      /** Action definitions (optional) */\n      actions: s.map({\n        /** Zod schema for action params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    builtInActions: [\n      {\n        name: \"setState\",\n        description:\n          \"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }\",\n      },\n      {\n        name: \"pushState\",\n        description:\n          'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {\"$state\":\"/path\"} refs and \"$id\" for auto IDs.',\n      },\n      {\n        name: \"removeState\",\n        description:\n          \"Remove an item from an array in state by index. Params: { statePath: string, index: number }\",\n      },\n      {\n        name: \"validateForm\",\n        description:\n          \"Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.\",\n      },\n    ],\n    defaultRules: [\n      // Element integrity\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n      \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n\n      // Field placement\n      'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"},\"children\":[...]}.',\n      'CRITICAL: The \"on\" field goes on the ELEMENT object, NOT inside \"props\". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',\n\n      // State and data\n      \"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).\",\n      'When building repeating content backed by a state array (e.g. posts, products, items), use the \"repeat\" field on a container element. Example: { \"type\": \"<ContainerComponent>\", \"props\": {}, \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" }, \"children\": [\"post-card\"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n      // Design quality\n      \"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.\",\n    ],\n  },\n);\n\n/**\n * Type for the Solid schema\n */\nexport type SolidSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type SolidSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n\n// Backward compatibility aliases\n/** @deprecated Use `schema` instead */\nexport const elementTreeSchema = schema;\n/** @deprecated Use `SolidSchema` instead */\nexport type ElementTreeSchema = SolidSchema;\n/** @deprecated Use `SolidSpec` instead */\nexport type ElementTreeSpec<T> = SolidSpec<T>;\n"
  },
  {
    "path": "packages/solid/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/solid/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\nimport { solidPlugin } from \"esbuild-plugin-solid\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/schema.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  esbuildPlugins: [solidPlugin({ solid: { generate: \"dom\" } })],\n  external: [\"solid-js\", \"@json-render/core\"],\n});\n"
  },
  {
    "path": "packages/svelte/CHANGELOG.md",
    "content": "# @json-render/svelte\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Minor Changes\n\n- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.\n\n  ### New:\n  - **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.\n  - **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.\n  - **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.\n\n  ### Fixed:\n  - **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n"
  },
  {
    "path": "packages/svelte/README.md",
    "content": "# @json-render/svelte\n\nSvelte 5 renderer for `@json-render/core`. Turn JSON specs into Svelte components with runes-based reactivity.\n\n## Installation\n\n```bash\nnpm install @json-render/core @json-render/svelte\n```\n\nPeer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.\n\n## Quick Start\n\n### 1. Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/svelte/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"A card container\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n      }),\n      description: \"A clickable button\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit the form\" },\n  },\n});\n```\n\n### 2. Define Component Implementations\n\n```typescript\nimport { defineRegistry } from \"@json-render/svelte\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => /* Svelte 5 snippet */,\n    Button: ({ props, emit }) => /* Svelte 5 snippet */,\n  },\n  actions: {\n    submit: async (params) => { /* handle submit */ },\n  },\n});\n```\n\n### 3. Render Specs\n\n```svelte\n<script>\n  import { Renderer, StateProvider, ActionProvider } from \"@json-render/svelte\";\n  import { registry } from \"./registry\";\n\n  const spec = { /* ... */ };\n</script>\n\n<StateProvider initialState={{ form: { name: \"\" } }}>\n  <ActionProvider handlers={{ submit: handleSubmit }}>\n    <Renderer {spec} {registry} />\n  </ActionProvider>\n</StateProvider>\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| `RepeatScopeProvider` | Repeat scope for list rendering |\n| `FunctionsContextProvider` | Register `$computed` functions |\n| `JsonUIProvider` | All-in-one provider combining state, actions, visibility, validation, and functions |\n\n## Context Accessors\n\nSvelte 5 uses `getContext`-based accessors instead of hooks:\n\n| Accessor | Purpose |\n|----------|---------|\n| `getStateContext()` | Access state context (`state`, `get`, `set`) |\n| `getStateValue(path)` | Get single value from state |\n| `getBoundProp(value, binding)` | Two-way binding for `$bindState`/`$bindItem` |\n| `getVisibilityContext()` | Access visibility context |\n| `isVisible(condition)` | Check if a visibility condition is met |\n| `getActionContext()` | Access action context |\n| `getAction(binding)` | Get a single action dispatch function |\n| `getValidationContext()` | Access validation context |\n| `getFieldValidation(path, config)` | Field validation state |\n| `getRepeatScope()` | Access current repeat scope |\n| `getFunctions()` | Access registered computed functions |\n\n## Streaming\n\n```typescript\nimport { createUIStream, createChatUI } from \"@json-render/svelte\";\n\nconst stream = createUIStream({ endpoint: \"/api/generate\" });\n\n// Or for chat-style UI:\nconst chat = createChatUI({ endpoint: \"/api/chat\" });\n```\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `defineRegistry` | Create a type-safe component registry from a catalog |\n| `createRenderer` | Create a renderer function from a registry |\n| `Renderer` | Render a spec using a registry |\n| `CatalogRenderer` | Render with automatic provider wiring |\n| `JsonUIProvider` | All-in-one provider |\n| `schema` | Element tree schema (includes built-in state actions) |\n| `createUIStream` | Stream specs from an API endpoint |\n| `createChatUI` | Chat-style streaming interface |\n| `flatToTree` | Convert flat spec to tree format |\n| `ConfirmDialog` | Confirmation dialog component |\n| `ConfirmDialogManager` | Manage multiple confirmation dialogs |\n\n### Types\n\n| Export | Purpose |\n|--------|---------|\n| `ComponentContext` | Typed component render function context (catalog-aware) |\n| `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries |\n| `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` |\n| `ComponentFn` | Component render function type |\n| `SetState` | State setter type |\n| `StateModel` | State model type |\n| `SvelteSchema` | Schema type for the Svelte renderer |\n| `SvelteSpec` | Spec type for the Svelte renderer |\n\n## Documentation\n\nFull API reference: [json-render.dev/docs/api/svelte](https://json-render.dev/docs/api/svelte).\n\n## License\n\nApache-2.0\n"
  },
  {
    "path": "packages/svelte/package.json",
    "content": "{\n  \"name\": \"@json-render/svelte\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"svelte\",\n    \"svelte5\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\",\n    \"runes\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/svelte\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"type\": \"module\",\n  \"svelte\": \"./dist/index.js\",\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"svelte\": \"./dist/index.js\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./schema\": {\n      \"types\": \"./dist/schema.d.ts\",\n      \"svelte\": \"./dist/schema.js\",\n      \"default\": \"./dist/schema.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"svelte-package -i src -o dist\",\n    \"dev\": \"svelte-package -i src -o dist --watch\",\n    \"typecheck\": \"svelte-check --tsconfig ./tsconfig.json\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@sveltejs/package\": \"^2.3.0\",\n    \"@sveltejs/vite-plugin-svelte\": \"^6.2.4\",\n    \"svelte\": \"^5.0.0\",\n    \"svelte-check\": \"^4.0.0\",\n    \"typescript\": \"^5.4.5\"\n  },\n  \"peerDependencies\": {\n    \"svelte\": \"^5.0.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/svelte/src/CatalogRenderer.svelte",
    "content": "<script module lang=\"ts\">\n  import type { ComputedFunction, Spec, StateStore } from \"@json-render/core\";\n  import type { ComponentRenderer, ComponentRegistry } from \"./renderer.js\";\n\n  export interface CatalogRendererProps {\n    spec: Spec | null;\n    registry: ComponentRegistry;\n    store?: StateStore;\n    state?: Record<string, unknown>;\n    onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n    onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n    functions?: Record<string, ComputedFunction>;\n    loading?: boolean;\n    fallback?: ComponentRenderer;\n  }\n</script>\n\n<script lang=\"ts\">\n  import type { ActionHandler } from \"@json-render/core\";\n  import JsonUIProvider from \"./JsonUIProvider.svelte\";\n  import Renderer from \"./Renderer.svelte\";\n\n  let {\n    spec,\n    registry,\n    store,\n    state,\n    onAction,\n    onStateChange,\n    functions,\n    loading = false,\n    fallback,\n  }: CatalogRendererProps = $props();\n\n  let actionHandlers = $derived.by(() => {\n    if (!onAction) return undefined;\n    // Wrap onAction with a Proxy so any action name routes to the callback\n    return new Proxy({} as Record<string, ActionHandler>, {\n      get: (_target, prop: string) => {\n        return (params: Record<string, unknown>) => onAction(prop, params);\n      },\n      has: () => true,\n    });\n  });\n</script>\n\n<JsonUIProvider\n  {registry}\n  {store}\n  initialState={state}\n  handlers={actionHandlers}\n  {functions}\n  {onStateChange}>\n  <Renderer {spec} {registry} {loading} {fallback} />\n</JsonUIProvider>\n"
  },
  {
    "path": "packages/svelte/src/ConfirmDialog.svelte",
    "content": "<script lang=\"ts\">\n  import type { ActionConfirm } from \"@json-render/core\";\n\n  interface Props {\n    confirm: ActionConfirm;\n    onConfirm: () => void;\n    onCancel: () => void;\n  }\n\n  let { confirm, onConfirm, onCancel }: Props = $props();\n\n  let isDanger = $derived(confirm.variant === \"danger\");\n</script>\n\n<!-- svelte-ignore a11y_click_events_have_key_events -->\n<!-- svelte-ignore a11y_no_static_element_interactions -->\n<div\n  class=\"overlay\"\n  onclick={onCancel}\n>\n  <!-- svelte-ignore a11y_click_events_have_key_events -->\n  <!-- svelte-ignore a11y_no_static_element_interactions -->\n  <div class=\"dialog\" onclick={(e) => e.stopPropagation()}>\n    <h3 class=\"title\">{confirm.title}</h3>\n    <p class=\"message\">{confirm.message}</p>\n    <div class=\"buttons\">\n      <button class=\"cancel-btn\" onclick={onCancel}>\n        {confirm.cancelLabel ?? \"Cancel\"}\n      </button>\n      <button\n        class=\"confirm-btn\"\n        class:danger={isDanger}\n        onclick={onConfirm}\n      >\n        {confirm.confirmLabel ?? \"Confirm\"}\n      </button>\n    </div>\n  </div>\n</div>\n\n<style>\n  .overlay {\n    position: fixed;\n    inset: 0;\n    background-color: rgba(0, 0, 0, 0.5);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 50;\n  }\n\n  .dialog {\n    background-color: white;\n    border-radius: 8px;\n    padding: 24px;\n    max-width: 400px;\n    width: 100%;\n    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);\n  }\n\n  .title {\n    margin: 0 0 8px 0;\n    font-size: 18px;\n    font-weight: 600;\n  }\n\n  .message {\n    margin: 0 0 24px 0;\n    color: #6b7280;\n  }\n\n  .buttons {\n    display: flex;\n    gap: 12px;\n    justify-content: flex-end;\n  }\n\n  .cancel-btn {\n    padding: 8px 16px;\n    border-radius: 6px;\n    border: 1px solid #d1d5db;\n    background-color: white;\n    cursor: pointer;\n  }\n\n  .confirm-btn {\n    padding: 8px 16px;\n    border-radius: 6px;\n    border: none;\n    background-color: #3b82f6;\n    color: white;\n    cursor: pointer;\n  }\n\n  .confirm-btn.danger {\n    background-color: #dc2626;\n  }\n</style>\n"
  },
  {
    "path": "packages/svelte/src/ConfirmDialogManager.svelte",
    "content": "<script lang=\"ts\">\n  import ConfirmDialog from \"./ConfirmDialog.svelte\";\n  import { getActionContext } from \"./contexts/ActionProvider.svelte\";\n\n  const actionCtx = getActionContext();\n  let pendingConfirmation = $derived(actionCtx.pendingConfirmation);\n</script>\n\n{#if pendingConfirmation?.action.confirm}\n  <ConfirmDialog\n    confirm={pendingConfirmation.action.confirm}\n    onConfirm={actionCtx.confirm}\n    onCancel={actionCtx.cancel}\n  />\n{/if}\n"
  },
  {
    "path": "packages/svelte/src/ElementRenderer.svelte",
    "content": "<script lang=\"ts\">\n  import type { Spec, UIElement } from \"@json-render/core\";\n  import {\n    resolveElementProps,\n    resolveBindings,\n    resolveActionParam,\n    evaluateVisibility,\n    type PropResolutionContext,\n  } from \"@json-render/core\";\n  import type { ComponentRegistry, ComponentRenderer } from \"./renderer.js\";\n  import type { EventHandle } from \"./catalog-types.js\";\n  import { getStateContext } from \"./contexts/StateProvider.svelte\";\n  import { getActionContext } from \"./contexts/ActionProvider.svelte\";\n  import { getRepeatScope } from \"./contexts/RepeatScopeProvider.svelte\";\n  import { getFunctions } from \"./contexts/FunctionsContextProvider.svelte\";\n  import RepeatChildren from \"./RepeatChildren.svelte\";\n  import Self from \"./ElementRenderer.svelte\";\n\n  interface Props {\n    element: UIElement;\n    spec: Spec;\n    registry: ComponentRegistry;\n    loading?: boolean;\n    fallback?: ComponentRenderer;\n  }\n\n  let { element, spec, registry, loading = false, fallback }: Props = $props();\n\n  const stateCtx = getStateContext();\n  const actionCtx = getActionContext();\n  const repeatScope = getRepeatScope();\n  const functions = getFunctions();\n\n  // Build context with repeat scope and $computed functions\n  let fullCtx = $derived<PropResolutionContext>(\n    repeatScope\n      ? {\n          stateModel: stateCtx.state,\n          repeatItem: repeatScope.item,\n          repeatIndex: repeatScope.index,\n          repeatBasePath: repeatScope.basePath,\n          functions,\n        }\n      : { stateModel: stateCtx.state, functions },\n  );\n\n  // Evaluate visibility\n  let isVisible = $derived(\n    element.visible === undefined\n      ? true\n      : evaluateVisibility(element.visible, fullCtx),\n  );\n\n  // Resolve props and bindings\n  let rawProps = $derived(element.props as Record<string, unknown>);\n  let resolvedProps = $derived(resolveElementProps(rawProps, fullCtx));\n  let elementBindings = $derived(resolveBindings(rawProps, fullCtx));\n\n  // Create resolved element\n  let resolvedElement = $derived(\n    resolvedProps !== element.props\n      ? { ...element, props: resolvedProps }\n      : element,\n  );\n\n  // Get the component renderer\n  let Component = $derived(registry[resolvedElement.type] ?? fallback);\n\n  // Create emit function\n  function emit(eventName: string): void {\n    const binding = element.on?.[eventName];\n    if (!binding) return;\n\n    const actionBindings = Array.isArray(binding) ? binding : [binding];\n    for (const b of actionBindings) {\n      if (!b.params) {\n        actionCtx.execute(b);\n        continue;\n      }\n      // Resolve all action params\n      const resolved: Record<string, unknown> = {};\n      for (const [key, val] of Object.entries(b.params)) {\n        resolved[key] = resolveActionParam(val, fullCtx);\n      }\n      actionCtx.execute({ ...b, params: resolved });\n    }\n  }\n\n  function on(eventName: string): EventHandle {\n    const binding = element.on?.[eventName];\n    if (!binding) {\n      return { emit: () => {}, shouldPreventDefault: false, bound: false };\n    }\n\n    const actionBindings = Array.isArray(binding) ? binding : [binding];\n    const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);\n    return {\n      emit: () => emit(eventName),\n      shouldPreventDefault,\n      bound: true,\n    };\n  }\n</script>\n\n{#if isVisible && Component}\n  <svelte:boundary\n    onerror={(error) => {\n      console.error(\n        `[json-render] Rendering error in <${resolvedElement.type}>:`,\n        error,\n      );\n    }}>\n    <Component\n      element={resolvedElement}\n      bindings={elementBindings}\n      {loading}\n      {on}\n      {emit}>\n      {#if resolvedElement.repeat}\n        <RepeatChildren\n          element={resolvedElement}\n          {spec}\n          {registry}\n          {loading}\n          {fallback} />\n      {:else if resolvedElement.children}\n        {#each resolvedElement.children as childKey (childKey)}\n          {#if spec.elements[childKey]}\n            <Self\n              element={spec.elements[childKey]}\n              {spec}\n              {registry}\n              {loading}\n              {fallback} />\n          {:else if !loading}\n            {console.warn(\n              `[json-render] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\". This element will not render.`,\n            )}\n          {/if}\n        {/each}\n      {/if}\n    </Component>\n    {#snippet failed()}\n      <!-- render nothing -->\n    {/snippet}\n  </svelte:boundary>\n{/if}\n"
  },
  {
    "path": "packages/svelte/src/JsonUIProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import type { ComputedFunction } from \"@json-render/core\";\n  /**\n   * Props for JSONUIProvider\n   */\n  export interface JSONUIProviderProps {\n    /** Component registry (passed through for convenience; not used internally) */\n    registry?: ComponentRegistry;\n    /**\n     * External store (controlled mode). When provided, `initialState` and\n     * `onStateChange` are ignored.\n     */\n    store?: StateStore;\n    /** Initial state model */\n    initialState?: Record<string, unknown>;\n    /** Action handlers */\n    handlers?: Record<string, ActionHandler>;\n    /** Navigation function */\n    navigate?: (path: string) => void;\n    /** Custom validation functions */\n    validationFunctions?: Record<\n      string,\n      (value: unknown, args?: Record<string, unknown>) => boolean\n    >;\n    /** Named functions for `$computed` expressions in props */\n    functions?: Record<string, ComputedFunction>;\n    /** Callback when state changes (uncontrolled mode) */\n    onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n    /** Children snippet */\n    children: Snippet;\n  }\n</script>\n\n<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { ActionHandler, StateStore } from \"@json-render/core\";\n  import StateProvider from \"./contexts/StateProvider.svelte\";\n  import VisibilityProvider from \"./contexts/VisibilityProvider.svelte\";\n  import ValidationProvider from \"./contexts/ValidationProvider.svelte\";\n  import ActionProvider from \"./contexts/ActionProvider.svelte\";\n  import FunctionsContextProvider from \"./contexts/FunctionsContextProvider.svelte\";\n  import ConfirmDialogManager from \"./ConfirmDialogManager.svelte\";\n  import type { ComponentRegistry } from \"./renderer.js\";\n\n  let {\n    store,\n    initialState = {},\n    handlers = {},\n    navigate,\n    validationFunctions = {},\n    functions,\n    onStateChange,\n    children,\n  }: JSONUIProviderProps = $props();\n</script>\n\n<StateProvider {store} {initialState} {onStateChange}>\n  <VisibilityProvider>\n    <ValidationProvider customFunctions={validationFunctions}>\n      <ActionProvider {handlers} {navigate}>\n        <FunctionsContextProvider {functions}>\n          {@render children()}\n          <ConfirmDialogManager />\n        </FunctionsContextProvider>\n      </ActionProvider>\n    </ValidationProvider>\n  </VisibilityProvider>\n</StateProvider>\n"
  },
  {
    "path": "packages/svelte/src/Renderer.svelte",
    "content": "<script module lang=\"ts\">\n  import type { Spec } from \"@json-render/core\";\n  import ElementRenderer from \"./ElementRenderer.svelte\";\n  import type { ComponentRegistry, ComponentRenderer } from \"./renderer.js\";\n\n  /**\n   * Props for the Renderer component\n   */\n  export interface RendererProps {\n    /** The UI spec to render */\n    spec: Spec | null;\n    /** Component registry */\n    registry: ComponentRegistry;\n    /** Whether the spec is currently loading/streaming */\n    loading?: boolean;\n    /** Fallback component for unknown types */\n    fallback?: ComponentRenderer;\n  }\n</script>\n\n<script lang=\"ts\">\n  let { spec, registry, loading = false, fallback }: RendererProps = $props();\n\n  let rootElement = $derived(spec?.root ? spec.elements[spec.root] : undefined);\n</script>\n\n{#if spec && rootElement}\n  <ElementRenderer\n    element={rootElement}\n    {spec}\n    {registry}\n    {loading}\n    {fallback} />\n{/if}\n"
  },
  {
    "path": "packages/svelte/src/RendererWithProvider.test.svelte",
    "content": "<script lang=\"ts\">\n  import type { Spec, ActionHandler } from \"@json-render/core\";\n  import type { ComponentRegistry, ComponentRenderer } from \"./renderer.js\";\n  import StateProvider from \"./contexts/StateProvider.svelte\";\n  import VisibilityProvider from \"./contexts/VisibilityProvider.svelte\";\n  import ValidationProvider from \"./contexts/ValidationProvider.svelte\";\n  import ActionProvider from \"./contexts/ActionProvider.svelte\";\n  import Renderer from \"./Renderer.svelte\";\n\n  interface Props {\n    spec: Spec | null;\n    registry: ComponentRegistry;\n    loading?: boolean;\n    fallback?: ComponentRenderer;\n    initialState?: Record<string, unknown>;\n    handlers?: Record<string, ActionHandler>;\n  }\n\n  let {\n    spec,\n    registry,\n    loading = false,\n    fallback,\n    initialState = {},\n    handlers = {},\n  }: Props = $props();\n</script>\n\n<StateProvider {initialState}>\n  <VisibilityProvider>\n    <ValidationProvider>\n      <ActionProvider {handlers}>\n        <Renderer {spec} {registry} {loading} {fallback} />\n      </ActionProvider>\n    </ValidationProvider>\n  </VisibilityProvider>\n</StateProvider>\n"
  },
  {
    "path": "packages/svelte/src/RepeatChildren.svelte",
    "content": "<script lang=\"ts\">\n  import type { Spec, UIElement } from \"@json-render/core\";\n  import { getByPath } from \"@json-render/core\";\n  import type { ComponentRegistry, ComponentRenderer } from \"./renderer.js\";\n  import { getStateContext } from \"./contexts/StateProvider.svelte\";\n  import RepeatScopeProvider from \"./contexts/RepeatScopeProvider.svelte\";\n  import ElementRenderer from \"./ElementRenderer.svelte\";\n\n  interface Props {\n    element: UIElement;\n    spec: Spec;\n    registry: ComponentRegistry;\n    loading?: boolean;\n    fallback?: ComponentRenderer;\n  }\n\n  let { element, spec, registry, loading = false, fallback }: Props = $props();\n\n  const stateCtx = getStateContext();\n\n  // Get items from state\n  let items = $derived(\n    (getByPath(stateCtx.state, element.repeat!.statePath) as\n      | unknown[]\n      | undefined) ?? [],\n  );\n</script>\n\n{#each items as itemValue, index (element.repeat?.key && typeof itemValue === \"object\" && itemValue !== null ? String((itemValue as any)[element.repeat.key] ?? index) : String(index))}\n  {@const basePath = `${element.repeat!.statePath}/${index}`}\n\n  {#if element.children}\n    <RepeatScopeProvider item={itemValue} {index} {basePath}>\n      {#each element.children as childKey (childKey)}\n        {#if spec.elements[childKey]}\n          <ElementRenderer\n            element={spec.elements[childKey]}\n            {spec}\n            {registry}\n            {loading}\n            {fallback} />\n        {:else if !loading}\n          {console.warn(\n            `[json-render] Missing element \"${childKey}\" referenced as child of \"${element.type}\". This element will not render.`,\n          )}\n        {/if}\n      {/each}\n    </RepeatScopeProvider>\n  {/if}\n{/each}\n"
  },
  {
    "path": "packages/svelte/src/TestButton.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"./catalog-types.js\";\n\n  interface Props extends BaseComponentProps<{ label?: string }> {}\n\n  let { props, emit, bindings, loading }: Props = $props();\n</script>\n\n<button class=\"test-button\" onclick={() => emit(\"press\")}>\n  {props.label ?? \"Button\"}\n</button>\n"
  },
  {
    "path": "packages/svelte/src/TestContainer.svelte",
    "content": "<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"./catalog-types.js\";\n\n  interface Props extends BaseComponentProps<{ title?: string }> {\n    children?: Snippet;\n  }\n\n  let { props, children, emit, bindings, loading }: Props = $props();\n</script>\n\n<div class=\"test-container\" data-loading={loading}>\n  {#if props.title}\n    <h2>{props.title}</h2>\n  {/if}\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n"
  },
  {
    "path": "packages/svelte/src/TestText.svelte",
    "content": "<script lang=\"ts\">\n  import type { BaseComponentProps } from \"./catalog-types.js\";\n\n  interface Props extends BaseComponentProps<{ text?: string }> {}\n\n  let { props, emit, bindings, loading }: Props = $props();\n</script>\n\n<span class=\"test-text\">{props.text ?? \"\"}</span>\n"
  },
  {
    "path": "packages/svelte/src/catalog-types.ts",
    "content": "import type { Component, Snippet } from \"svelte\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\n/**\n * State setter function for updating application state\n */\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\n/**\n * Handle returned by the `on()` function for a specific event.\n * Provides metadata about the event binding and a method to fire it.\n */\nexport interface EventHandle {\n  /** Fire the event (resolve action bindings) */\n  emit: () => void;\n  /** Whether any binding requested preventDefault */\n  shouldPreventDefault: boolean;\n  /** Whether any handler is bound to this event */\n  bound: boolean;\n}\n\n/**\n * Catalog-agnostic base type for component render function arguments.\n * Use this when building reusable component libraries.\n */\nexport interface BaseComponentProps<P = Record<string, unknown>> {\n  props: P;\n  children?: Snippet;\n  /** Simple event emitter (shorthand). Fires the event and returns void. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\n/**\n * Context passed to component render functions\n */\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> extends BaseComponentProps<InferComponentProps<C, K>> {}\n\n/**\n * Component render function type for Svelte\n */\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = Component<BaseComponentProps<InferComponentProps<C, K>>>;\n\n/**\n * Registry of Svelte component constructors for a catalog\n */\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n\n// =============================================================================\n// Action Types\n// =============================================================================\n\n/**\n * Action handler function type\n */\nexport type ActionFn<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = (\n  params: InferActionParams<C, K> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Registry of all action handlers for a catalog\n */\nexport type Actions<C extends Catalog> = {\n  [K in keyof InferCatalogActions<C>]: ActionFn<C, K>;\n};\n"
  },
  {
    "path": "packages/svelte/src/contexts/ActionProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext } from \"svelte\";\n  import type {\n    ActionBinding,\n    ActionConfirm,\n    ActionHandler,\n    ResolvedAction,\n  } from \"@json-render/core\";\n\n  const ACTION_KEY = Symbol.for(\"json-render-actions\");\n\n  /**\n   * Pending confirmation state\n   */\n  export interface PendingConfirmation {\n    action: ResolvedAction;\n    handler: ActionHandler;\n    resolve: () => void;\n    reject: () => void;\n  }\n\n  export interface CurrentValue<T> {\n    readonly current: T;\n  }\n\n  /**\n   * Action context value\n   */\n  export interface ActionContext {\n    /** Registered action handlers */\n    handlers: Record<string, ActionHandler>;\n    /** Currently loading action names */\n    loadingActions: Set<string>;\n    /** Pending confirmation dialog */\n    pendingConfirmation: PendingConfirmation | null;\n    /** Execute an action binding */\n    execute: (binding: ActionBinding) => Promise<void>;\n    /** Confirm the pending action */\n    confirm: () => void;\n    /** Cancel the pending action */\n    cancel: () => void;\n    /** Register an action handler */\n    registerHandler: (name: string, handler: ActionHandler) => void;\n  }\n\n  /**\n   * Get the action context from component tree\n   */\n  export function getActionContext(): ActionContext {\n    const ctx = getContext<ActionContext>(ACTION_KEY);\n    if (!ctx) {\n      throw new Error(\n        \"getActionContext must be called within an ActionProvider\",\n      );\n    }\n    return ctx;\n  }\n\n  /**\n   * Convenience helper to get a registered action handler by name\n   */\n  export function getAction(\n    name: string,\n  ): CurrentValue<ActionHandler | undefined> {\n    const context = getActionContext();\n    return {\n      get current() {\n        return context.handlers[name];\n      },\n    };\n  }\n\n  /**\n   * Props for ConfirmDialog component\n   */\n  export interface ConfirmDialogProps {\n    /** The confirmation config */\n    confirm: ActionConfirm;\n    /** Called when confirmed */\n    onConfirm: () => void;\n    /** Called when cancelled */\n    onCancel: () => void;\n  }\n\n  /**\n   * Generate a unique ID for use with the \"$id\" token.\n   */\n  let idCounter = 0;\n  function generateUniqueId(): string {\n    idCounter += 1;\n    return `${Date.now()}-${idCounter}`;\n  }\n\n  /**\n   * Deep-resolve dynamic value references within an object.\n   */\n  function deepResolveValue(\n    value: unknown,\n    get: (path: string) => unknown,\n  ): unknown {\n    if (value === null || value === undefined) return value;\n\n    if (value === \"$id\") {\n      return generateUniqueId();\n    }\n\n    if (typeof value === \"object\" && !Array.isArray(value)) {\n      const obj = value as Record<string, unknown>;\n      const keys = Object.keys(obj);\n\n      if (keys.length === 1 && typeof obj.$state === \"string\") {\n        return get(obj.$state as string);\n      }\n\n      if (keys.length === 1 && \"$id\" in obj) {\n        return generateUniqueId();\n      }\n    }\n\n    if (Array.isArray(value)) {\n      return value.map((item) => deepResolveValue(item, get));\n    }\n\n    if (typeof value === \"object\") {\n      const resolved: Record<string, unknown> = {};\n      for (const [key, val] of Object.entries(\n        value as Record<string, unknown>,\n      )) {\n        resolved[key] = deepResolveValue(val, get);\n      }\n      return resolved;\n    }\n\n    return value;\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext, type Snippet } from \"svelte\";\n  import {\n    resolveAction,\n    executeAction,\n    type ActionBinding as CoreActionBinding,\n    type ActionHandler as CoreActionHandler,\n  } from \"@json-render/core\";\n  import { getStateContext } from \"./StateProvider.svelte\";\n  import { getOptionalValidationContext } from \"./ValidationProvider.svelte\";\n  import { SvelteSet } from \"svelte/reactivity\";\n\n  interface Props {\n    handlers?: Record<string, CoreActionHandler>;\n    navigate?: (path: string) => void;\n    children?: Snippet;\n  }\n\n  let { handlers = {}, navigate, children }: Props = $props();\n\n  const stateCtx = getStateContext();\n  const validation = getOptionalValidationContext();\n\n  let registeredHandlers = $state.raw<Record<string, CoreActionHandler>>({});\n  let loadingActions = new SvelteSet<string>();\n  let pendingConfirmation = $state.raw<PendingConfirmation | null>(null);\n\n  const execute = async (binding: CoreActionBinding): Promise<void> => {\n    const resolved = resolveAction(binding, stateCtx.getSnapshot());\n\n    if (resolved.action === \"setState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const value = resolved.params.value;\n      if (statePath) {\n        stateCtx.set(statePath, value);\n      }\n      return;\n    }\n\n    if (resolved.action === \"pushState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const rawValue = resolved.params.value;\n      if (statePath) {\n        const resolvedValue = deepResolveValue(rawValue, stateCtx.get);\n        const arr = (stateCtx.get(statePath) as unknown[] | undefined) ?? [];\n        stateCtx.set(statePath, [...arr, resolvedValue]);\n        const clearStatePath = resolved.params.clearStatePath as\n          | string\n          | undefined;\n        if (clearStatePath) {\n          stateCtx.set(clearStatePath, \"\");\n        }\n      }\n      return;\n    }\n\n    if (resolved.action === \"removeState\" && resolved.params) {\n      const statePath = resolved.params.statePath as string;\n      const index = resolved.params.index as number;\n      if (statePath !== undefined && index !== undefined) {\n        const arr = (stateCtx.get(statePath) as unknown[] | undefined) ?? [];\n        stateCtx.set(\n          statePath,\n          arr.filter((_, i) => i !== index),\n        );\n      }\n      return;\n    }\n\n    if (resolved.action === \"validateForm\") {\n      if (!validation?.validateAll) {\n        console.warn(\n          \"validateForm action was dispatched but no ValidationProvider is connected. \" +\n            \"Ensure ValidationProvider is rendered inside the provider tree.\",\n        );\n        return;\n      }\n      const valid = validation.validateAll();\n      const errors: Record<string, string[]> = {};\n      for (const [path, fieldState] of Object.entries(validation.fieldStates)) {\n        if (fieldState.result && !fieldState.result.valid) {\n          errors[path] = fieldState.result.errors;\n        }\n      }\n      const statePath =\n        (resolved.params?.statePath as string) || \"/formValidation\";\n      stateCtx.set(statePath, { valid, errors });\n      return;\n    }\n\n    if (resolved.action === \"push\" && resolved.params) {\n      const screen = resolved.params.screen as string;\n      if (screen) {\n        const currentScreen = stateCtx.get(\"/currentScreen\") as\n          | string\n          | undefined;\n        const navStack =\n          (stateCtx.get(\"/navStack\") as string[] | undefined) ?? [];\n        if (currentScreen) {\n          stateCtx.set(\"/navStack\", [...navStack, currentScreen]);\n        } else {\n          stateCtx.set(\"/navStack\", [...navStack, \"\"]);\n        }\n        stateCtx.set(\"/currentScreen\", screen);\n      }\n      return;\n    }\n\n    if (resolved.action === \"pop\") {\n      const navStack =\n        (stateCtx.get(\"/navStack\") as string[] | undefined) ?? [];\n      if (navStack.length > 0) {\n        const previousScreen = navStack[navStack.length - 1];\n        stateCtx.set(\"/navStack\", navStack.slice(0, -1));\n        if (previousScreen) {\n          stateCtx.set(\"/currentScreen\", previousScreen);\n        } else {\n          stateCtx.set(\"/currentScreen\", undefined);\n        }\n      }\n      return;\n    }\n\n    const handler =\n      registeredHandlers[resolved.action] ?? handlers[resolved.action];\n\n    if (!handler) {\n      console.warn(`No handler registered for action: ${resolved.action}`);\n      return;\n    }\n\n    if (resolved.confirm) {\n      return new Promise<void>((resolve, reject) => {\n        pendingConfirmation = {\n          action: resolved,\n          handler,\n          resolve: () => {\n            pendingConfirmation = null;\n            resolve();\n          },\n          reject: () => {\n            pendingConfirmation = null;\n            reject(new Error(\"Action cancelled\"));\n          },\n        };\n      }).then(async () => {\n        loadingActions.add(resolved.action);\n        try {\n          await executeAction({\n            action: resolved,\n            handler,\n            setState: stateCtx.set,\n            navigate,\n            executeAction: async (name) => {\n              const subBinding: CoreActionBinding = { action: name };\n              await execute(subBinding);\n            },\n          });\n        } finally {\n          loadingActions.delete(resolved.action);\n        }\n      });\n    }\n\n    loadingActions.add(resolved.action);\n    try {\n      await executeAction({\n        action: resolved,\n        handler,\n        setState: stateCtx.set,\n        navigate,\n        executeAction: async (name) => {\n          const subBinding: CoreActionBinding = { action: name };\n          await execute(subBinding);\n        },\n      });\n    } finally {\n      loadingActions.delete(resolved.action);\n    }\n  };\n\n  const ctx: ActionContext = {\n    get handlers() {\n      return { ...handlers, ...registeredHandlers };\n    },\n    get loadingActions() {\n      return loadingActions;\n    },\n    get pendingConfirmation() {\n      return pendingConfirmation;\n    },\n    execute,\n    confirm: () => {\n      pendingConfirmation?.resolve();\n    },\n    cancel: () => {\n      pendingConfirmation?.reject();\n    },\n    registerHandler: (name: string, handler: CoreActionHandler) => {\n      registeredHandlers = { ...registeredHandlers, [name]: handler };\n    },\n  };\n\n  setContext(ACTION_KEY, ctx);\n</script>\n\n{@render children?.()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/FunctionsContextProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext } from \"svelte\";\n  import type { ComputedFunction } from \"@json-render/core\";\n\n  const FUNCTIONS_KEY = Symbol.for(\"json-render-functions\");\n  const EMPTY_FUNCTIONS: Record<string, ComputedFunction> = {};\n\n  /**\n   * Functions context value\n   */\n  export interface FunctionsContext {\n    /** Named functions for `$computed` expressions */\n    functions: Record<string, ComputedFunction>;\n  }\n\n  /**\n   * Get the functions context from component tree\n   */\n  export function getFunctions(): Record<string, ComputedFunction> {\n    const ctx = getContext<FunctionsContext>(FUNCTIONS_KEY);\n    return ctx?.functions ?? EMPTY_FUNCTIONS;\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext, type Snippet } from \"svelte\";\n\n  interface Props {\n    functions?: Record<string, ComputedFunction>;\n    children?: Snippet;\n  }\n\n  let { functions, children }: Props = $props();\n\n  const ctx: FunctionsContext = {\n    get functions() {\n      return functions ?? EMPTY_FUNCTIONS;\n    },\n  };\n\n  setContext(FUNCTIONS_KEY, ctx);\n</script>\n\n{@render children?.()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/RepeatScopeProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext, setContext, type Snippet } from \"svelte\";\n\n  const REPEAT_SCOPE_KEY = Symbol(\"json-render-repeat-scope\");\n\n  /**\n   * Repeat scope value provided to child elements inside a repeated element.\n   */\n  export interface RepeatScopeValue {\n    /** The current array item object */\n    item: unknown;\n    /** Index of the current item in the array */\n    index: number;\n    /** Absolute state path to the current array item (e.g. \"/todos/0\") */\n    basePath: string;\n  }\n\n  /**\n   * Get the current repeat scope (or null if not inside a repeated element)\n   */\n  export function getRepeatScope(): RepeatScopeValue | null {\n    return getContext<RepeatScopeValue | null>(REPEAT_SCOPE_KEY) ?? null;\n  }\n</script>\n\n<script lang=\"ts\">\n  interface Props {\n    item: unknown;\n    index: number;\n    basePath: string;\n    children: Snippet;\n  }\n\n  let { item, index, basePath, children }: Props = $props();\n\n  setContext(REPEAT_SCOPE_KEY, {\n    get item() {\n      return item;\n    },\n    get index() {\n      return index;\n    },\n    get basePath() {\n      return basePath;\n    },\n  });\n</script>\n\n{@render children()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/StateProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext } from \"svelte\";\n  import type { StateModel } from \"@json-render/core\";\n\n  const STATE_KEY = Symbol.for(\"json-render-state\");\n\n  /**\n   * State context value\n   */\n  export interface StateContext {\n    /** The current state model (reactive) */\n    readonly state: StateModel;\n    /** Get a value by path */\n    get: (path: string) => unknown;\n    /** Set a value by path */\n    set: (path: string, value: unknown) => void;\n    /** Update multiple values at once */\n    update: (updates: Record<string, unknown>) => void;\n    /** Return the live state snapshot from the underlying store. */\n    getSnapshot: () => StateModel;\n  }\n\n  export interface CurrentValue<T> {\n    current: T;\n  }\n\n  /**\n   * Get the state context from component tree\n   */\n  export function getStateContext(): StateContext {\n    const ctx = getContext<StateContext>(STATE_KEY);\n    if (!ctx) {\n      throw new Error(\"getStateContext must be called within a StateProvider\");\n    }\n    return ctx;\n  }\n\n  /**\n   * Convenience helper to read a value from the state context\n   */\n  export function getStateValue(path: string): CurrentValue<unknown> {\n    const context = getStateContext();\n    return {\n      get current() {\n        return context.get(path);\n      },\n      set current(value: unknown) {\n        context.set(path, value);\n      },\n    };\n  }\n\n  /**\n   * Two-way helper for `$bindState` / `$bindItem` bindings.\n   * Mirrors `useBoundProp` from React packages.\n   */\n  export function getBoundProp<T>(\n    propValue: () => T | undefined,\n    bindingPath: () => string | undefined,\n  ): CurrentValue<T | undefined> {\n    const context = getStateContext();\n    return {\n      get current() {\n        return propValue();\n      },\n      set current(value: T | undefined) {\n        const path = bindingPath();\n        if (path) {\n          context.set(path, value);\n        }\n      },\n    };\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext, type Snippet } from \"svelte\";\n  import {\n    createStateStore,\n    getByPath,\n    type StateModel as CoreStateModel,\n    type StateStore as CoreStateStore,\n  } from \"@json-render/core\";\n  import { flattenToPointers } from \"@json-render/core/store-utils\";\n\n  interface Props {\n    store?: CoreStateStore;\n    initialState?: CoreStateModel;\n    onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n    children?: Snippet;\n  }\n\n  let { store, initialState = {}, onStateChange, children }: Props = $props();\n\n  // svelte-ignore state_referenced_locally\n  const internalStore = createStateStore(initialState);\n\n  function activeStore(): CoreStateStore {\n    return store ?? internalStore;\n  }\n\n  // Keep a reactive copy of the current store snapshot.\n  let model: CoreStateModel = $state.raw(activeStore().getSnapshot());\n\n  $effect(() => {\n    const currentStore = activeStore();\n    model = currentStore.getSnapshot();\n    const unsubscribe = currentStore.subscribe(() => {\n      model = currentStore.getSnapshot();\n    });\n    return unsubscribe;\n  });\n\n  // In uncontrolled mode, support reactive initialState updates.\n  // svelte-ignore state_referenced_locally\n  let prevFlat: Record<string, unknown> =\n    initialState && Object.keys(initialState).length > 0\n      ? flattenToPointers(initialState)\n      : {};\n  $effect(() => {\n    if (store) return;\n    const nextInitialState = initialState ?? {};\n    const nextFlat =\n      Object.keys(nextInitialState).length > 0\n        ? flattenToPointers(nextInitialState)\n        : {};\n    const allKeys = new Set([\n      ...Object.keys(prevFlat),\n      ...Object.keys(nextFlat),\n    ]);\n    const updates: Record<string, unknown> = {};\n    for (const key of allKeys) {\n      if (prevFlat[key] !== nextFlat[key]) {\n        updates[key] = key in nextFlat ? nextFlat[key] : undefined;\n      }\n    }\n    prevFlat = nextFlat;\n    if (Object.keys(updates).length > 0) {\n      internalStore.update(updates);\n    }\n  });\n\n  const ctx: StateContext = {\n    get state() {\n      return model;\n    },\n    get: (path: string) => activeStore().get(path),\n    getSnapshot: () => activeStore().getSnapshot(),\n    set: (path: string, value: unknown) => {\n      const currentStore = activeStore();\n      const prev = currentStore.getSnapshot();\n      currentStore.set(path, value);\n      const next = currentStore.getSnapshot();\n      model = next;\n      if (!store && next !== prev) {\n        onStateChange?.([{ path, value }]);\n      }\n    },\n    update: (updates: Record<string, unknown>) => {\n      const currentStore = activeStore();\n      const prev = currentStore.getSnapshot();\n      currentStore.update(updates);\n      const next = currentStore.getSnapshot();\n      model = next;\n      if (!store && next !== prev) {\n        const changes: Array<{ path: string; value: unknown }> = [];\n        for (const [path, value] of Object.entries(updates)) {\n          if (getByPath(prev, path) !== value) {\n            changes.push({ path, value });\n          }\n        }\n        if (changes.length > 0) {\n          onStateChange?.(changes);\n        }\n      }\n    },\n  };\n\n  setContext(STATE_KEY, ctx);\n</script>\n\n{@render children?.()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/ValidationProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext, hasContext } from \"svelte\";\n  import type {\n    ValidationConfig,\n    ValidationFunction,\n    ValidationResult,\n  } from \"@json-render/core\";\n\n  const VALIDATION_KEY = Symbol.for(\"json-render-validation\");\n\n  /**\n   * Field validation state\n   */\n  export interface FieldValidationState {\n    touched: boolean;\n    validated: boolean;\n    result: ValidationResult | null;\n  }\n\n  /**\n   * Validation context value\n   */\n  export interface ValidationContext {\n    /** Custom validation functions from catalog */\n    customFunctions: Record<string, ValidationFunction>;\n    /** Validation state by field path */\n    fieldStates: Record<string, FieldValidationState>;\n    /** Validate a field */\n    validate: (path: string, config: ValidationConfig) => ValidationResult;\n    /** Mark field as touched */\n    touch: (path: string) => void;\n    /** Clear validation for a field */\n    clear: (path: string) => void;\n    /** Validate all fields */\n    validateAll: () => boolean;\n    /** Register field config */\n    registerField: (path: string, config: ValidationConfig) => void;\n  }\n\n  /**\n   * Get the validation context from component tree\n   */\n  export function getValidationContext(): ValidationContext {\n    const ctx = getContext<ValidationContext>(VALIDATION_KEY);\n    if (!ctx) {\n      throw new Error(\n        \"getValidationContext must be called within a ValidationProvider\",\n      );\n    }\n    return ctx;\n  }\n\n  /**\n   * Get validation context if present.\n   */\n  export function getOptionalValidationContext(): ValidationContext | null {\n    return hasContext(VALIDATION_KEY)\n      ? getContext<ValidationContext>(VALIDATION_KEY)\n      : null;\n  }\n\n  /**\n   * Helper to get field validation state\n   */\n  export function getFieldValidation(\n    ctx: ValidationContext,\n    path: string,\n    config?: ValidationConfig,\n  ): {\n    state: FieldValidationState;\n    validate: () => ValidationResult;\n    touch: () => void;\n    clear: () => void;\n    errors: string[];\n    isValid: boolean;\n  } {\n    const state = ctx.fieldStates[path] ?? {\n      touched: false,\n      validated: false,\n      result: null,\n    };\n\n    return {\n      state,\n      validate: () => ctx.validate(path, config ?? { checks: [] }),\n      touch: () => ctx.touch(path),\n      clear: () => ctx.clear(path),\n      errors: state.result?.errors ?? [],\n      isValid: state.result?.valid ?? true,\n    };\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext, type Snippet } from \"svelte\";\n  import {\n    runValidation,\n    type ValidationConfig as CoreValidationConfig,\n    type ValidationFunction as CoreValidationFunction,\n    type ValidationResult as CoreValidationResult,\n  } from \"@json-render/core\";\n  import { getStateContext } from \"./StateProvider.svelte\";\n\n  interface Props {\n    customFunctions?: Record<string, CoreValidationFunction>;\n    children?: Snippet;\n  }\n\n  let { customFunctions = {}, children }: Props = $props();\n\n  const stateCtx = getStateContext();\n\n  let fieldStates = $state.raw<Record<string, FieldValidationState>>({});\n  let fieldConfigs = $state.raw<Record<string, CoreValidationConfig>>({});\n\n  const validate = (\n    path: string,\n    config: CoreValidationConfig,\n  ): CoreValidationResult => {\n    const segments = path.split(\"/\").filter(Boolean);\n    let value: unknown = stateCtx.state;\n    for (const seg of segments) {\n      if (value != null && typeof value === \"object\") {\n        value = (value as Record<string, unknown>)[seg];\n      } else {\n        value = undefined;\n        break;\n      }\n    }\n\n    const result = runValidation(config, {\n      value,\n      stateModel: stateCtx.state,\n      customFunctions,\n    });\n\n    fieldStates = {\n      ...fieldStates,\n      [path]: {\n        touched: fieldStates[path]?.touched ?? true,\n        validated: true,\n        result,\n      },\n    };\n\n    return result;\n  };\n\n  const touch = (path: string): void => {\n    fieldStates = {\n      ...fieldStates,\n      [path]: {\n        ...fieldStates[path],\n        touched: true,\n        validated: fieldStates[path]?.validated ?? false,\n        result: fieldStates[path]?.result ?? null,\n      },\n    };\n  };\n\n  const clear = (path: string): void => {\n    const { [path]: _, ...rest } = fieldStates;\n    fieldStates = rest;\n  };\n\n  const validateAll = (): boolean => {\n    let allValid = true;\n    for (const [path, config] of Object.entries(fieldConfigs)) {\n      const result = validate(path, config);\n      if (!result.valid) {\n        allValid = false;\n      }\n    }\n    return allValid;\n  };\n\n  const registerField = (path: string, config: CoreValidationConfig): void => {\n    fieldConfigs = { ...fieldConfigs, [path]: config };\n  };\n\n  const ctx: ValidationContext = {\n    get customFunctions() {\n      return customFunctions;\n    },\n    get fieldStates() {\n      return fieldStates;\n    },\n    validate,\n    touch,\n    clear,\n    validateAll,\n    registerField,\n  };\n\n  setContext(VALIDATION_KEY, ctx);\n</script>\n\n{@render children?.()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/VisibilityProvider.svelte",
    "content": "<script module lang=\"ts\">\n  import { getContext } from \"svelte\";\n  import {\n    type VisibilityCondition,\n    type VisibilityContext as CoreVisibilityContext,\n  } from \"@json-render/core\";\n\n  const VISIBILITY_KEY = Symbol.for(\"json-render-visibility\");\n\n  /**\n   * Visibility context value\n   */\n  export interface VisibilityContext {\n    /** Evaluate a visibility condition */\n    isVisible: (condition: VisibilityCondition | undefined) => boolean;\n    /** The underlying visibility context (for advanced use) */\n    ctx: CoreVisibilityContext;\n  }\n\n  export interface CurrentValue<T> {\n    readonly current: T;\n  }\n\n  /**\n   * Get the visibility context from component tree\n   */\n  export function getVisibilityContext(): VisibilityContext {\n    const ctx = getContext<VisibilityContext>(VISIBILITY_KEY);\n    if (!ctx) {\n      throw new Error(\n        \"getVisibilityContext must be called within a VisibilityProvider\",\n      );\n    }\n    return ctx;\n  }\n\n  /**\n   * Convenience helper to evaluate visibility from context\n   */\n  export function isVisible(\n    condition: VisibilityCondition | undefined,\n  ): CurrentValue<boolean> {\n    const context = getVisibilityContext();\n    return {\n      get current() {\n        return context.isVisible(condition);\n      },\n    };\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext as setContextInstance, type Snippet } from \"svelte\";\n  import {\n    evaluateVisibility as evaluateVisibilityInstance,\n    type VisibilityCondition as VisibilityConditionInstance,\n    type VisibilityContext as CoreVisibilityContextInstance,\n  } from \"@json-render/core\";\n  import { getStateContext } from \"./StateProvider.svelte\";\n\n  interface Props {\n    children?: Snippet;\n  }\n\n  let { children }: Props = $props();\n  const stateCtx = getStateContext();\n\n  const ctx = {\n    get ctx(): CoreVisibilityContextInstance {\n      return { stateModel: stateCtx.state };\n    },\n    isVisible: (condition: VisibilityConditionInstance | undefined) => {\n      return evaluateVisibilityInstance(condition, {\n        stateModel: stateCtx.state,\n      });\n    },\n  };\n\n  setContextInstance(VISIBILITY_KEY, ctx);\n</script>\n\n{@render children?.()}\n"
  },
  {
    "path": "packages/svelte/src/contexts/actions.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { mount, unmount } from \"svelte\";\nimport StateProvider, { getStateContext } from \"./StateProvider.svelte\";\nimport ActionProvider, { getActionContext } from \"./ActionProvider.svelte\";\nimport ValidationProvider, {\n  getValidationContext,\n} from \"./ValidationProvider.svelte\";\n\nfunction component(\n  runTest: () => Promise<void>,\n  options: {\n    initialState?: Record<string, unknown>;\n    handlers?: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<unknown> | unknown\n    >;\n    withValidation?: boolean;\n  } = {},\n) {\n  return async () => {\n    let promise: Promise<void>;\n    const c = mount(\n      ((_anchor: any) => {\n        (StateProvider as any)(_anchor, {\n          initialState: options.initialState ?? {},\n          children: ((_inner: any) => {\n            if (options.withValidation) {\n              (ValidationProvider as any)(_inner, {\n                children: ((__inner: any) => {\n                  (ActionProvider as any)(__inner, {\n                    handlers: options.handlers ?? {},\n                    children: (() => {\n                      promise = runTest();\n                    }) as any,\n                  });\n                }) as any,\n              });\n              return;\n            }\n\n            (ActionProvider as any)(_inner, {\n              handlers: options.handlers ?? {},\n              children: (() => {\n                promise = runTest();\n              }) as any,\n            });\n          }) as any,\n        });\n      }) as any,\n      { target: document.body },\n    );\n    await promise!; // eslint-disable-line @typescript-eslint/no-non-null-assertion\n    unmount(c);\n  };\n}\n\ndescribe(\"createActionContext\", () => {\n  it(\n    \"executes built-in setState action\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n\n        await actionCtx.execute({\n          action: \"setState\",\n          params: { statePath: \"/count\", value: 5 },\n        });\n\n        expect(stateCtx.state.count).toBe(5);\n      },\n      { initialState: { count: 0 } },\n    ),\n  );\n\n  it(\n    \"executes built-in pushState action\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n\n        await actionCtx.execute({\n          action: \"pushState\",\n          params: { statePath: \"/items\", value: \"c\" },\n        });\n\n        expect(stateCtx.state.items).toEqual([\"a\", \"b\", \"c\"]);\n      },\n      { initialState: { items: [\"a\", \"b\"] } },\n    ),\n  );\n\n  it(\n    \"pushState creates array if missing\",\n    component(async () => {\n      const stateCtx = getStateContext();\n      const actionCtx = getActionContext();\n\n      await actionCtx.execute({\n        action: \"pushState\",\n        params: { statePath: \"/newList\", value: \"first\" },\n      });\n\n      expect(stateCtx.get(\"/newList\")).toEqual([\"first\"]);\n    }),\n  );\n\n  it(\n    \"executes built-in removeState action\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n\n        await actionCtx.execute({\n          action: \"removeState\",\n          params: { statePath: \"/items\", index: 1 },\n        });\n\n        expect(stateCtx.state.items).toEqual([\"a\", \"c\"]);\n      },\n      { initialState: { items: [\"a\", \"b\", \"c\"] } },\n    ),\n  );\n\n  it(\n    \"executes push navigation action\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n\n        await actionCtx.execute({\n          action: \"push\",\n          params: { screen: \"settings\" },\n        });\n\n        expect(stateCtx.get(\"/currentScreen\")).toBe(\"settings\");\n        expect(stateCtx.get(\"/navStack\")).toEqual([\"home\"]);\n      },\n      { initialState: { currentScreen: \"home\" } },\n    ),\n  );\n\n  it(\n    \"executes pop navigation action\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n\n        await actionCtx.execute({ action: \"pop\" });\n\n        expect(stateCtx.get(\"/currentScreen\")).toBe(\"home\");\n        expect(stateCtx.get(\"/navStack\")).toEqual([]);\n      },\n      { initialState: { currentScreen: \"settings\", navStack: [\"home\"] } },\n    ),\n  );\n\n  it(\n    \"executes custom handlers\",\n    (() => {\n      const customHandler = vi.fn().mockResolvedValue(undefined);\n      return component(\n        async () => {\n          const actionCtx = getActionContext();\n\n          await actionCtx.execute({\n            action: \"myAction\",\n            params: { foo: \"bar\" },\n          });\n\n          expect(customHandler).toHaveBeenCalledWith({ foo: \"bar\" });\n        },\n        { handlers: { myAction: customHandler } },\n      );\n    })(),\n  );\n\n  it(\n    \"warns when no handler registered\",\n    component(async () => {\n      const actionCtx = getActionContext();\n      const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n      await actionCtx.execute({ action: \"unknownAction\" });\n\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining(\"unknownAction\"),\n      );\n      warnSpy.mockRestore();\n    }),\n  );\n\n  it(\n    \"tracks loading state for actions\",\n    (() => {\n      let resolveHandler: () => void;\n      const slowHandler = vi.fn(\n        () =>\n          new Promise<void>((resolve) => {\n            resolveHandler = resolve;\n          }),\n      );\n      return component(\n        async () => {\n          const actionCtx = getActionContext();\n          const executePromise = actionCtx.execute({ action: \"slowAction\" });\n\n          expect(actionCtx.loadingActions.has(\"slowAction\")).toBe(true);\n\n          resolveHandler!();\n          await executePromise;\n\n          expect(actionCtx.loadingActions.has(\"slowAction\")).toBe(false);\n        },\n        {\n          handlers: {\n            slowAction: slowHandler,\n          },\n        },\n      );\n    })(),\n  );\n\n  it(\n    \"allows registering handlers dynamically\",\n    component(async () => {\n      const actionCtx = getActionContext();\n      const dynamicHandler = vi.fn();\n\n      actionCtx.registerHandler(\"dynamicAction\", dynamicHandler);\n      await actionCtx.execute({ action: \"dynamicAction\", params: { x: 1 } });\n\n      expect(dynamicHandler).toHaveBeenCalledWith({ x: 1 });\n    }),\n  );\n\n  it(\n    \"executes validateForm and writes result to /formValidation\",\n    component(\n      async () => {\n        const stateCtx = getStateContext();\n        const actionCtx = getActionContext();\n        const validationCtx = getValidationContext();\n\n        validationCtx.registerField(\"/form/email\", {\n          checks: [{ type: \"required\", message: \"Required\" }],\n        });\n\n        await actionCtx.execute({ action: \"validateForm\" });\n\n        expect(stateCtx.get(\"/formValidation\")).toEqual({\n          valid: false,\n          errors: { \"/form/email\": [\"Required\"] },\n        });\n      },\n      { withValidation: true },\n    ),\n  );\n\n  it(\n    \"validateForm defaults to warning when validation context is missing\",\n    component(async () => {\n      const actionCtx = getActionContext();\n      const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n      await actionCtx.execute({ action: \"validateForm\" });\n\n      expect(warnSpy).toHaveBeenCalledWith(\n        expect.stringContaining(\"validateForm action was dispatched\"),\n      );\n      warnSpy.mockRestore();\n    }),\n  );\n});\n"
  },
  {
    "path": "packages/svelte/src/contexts/state.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { mount, unmount } from \"svelte\";\nimport { createStateStore } from \"@json-render/core\";\nimport StateProvider, { getStateContext } from \"./StateProvider.svelte\";\n\nfunction component(\n  runTest: () => void,\n  props: {\n    initialState?: Record<string, unknown>;\n    onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n    store?: ReturnType<typeof createStateStore>;\n  } = {},\n) {\n  return () => {\n    const c = mount(\n      ((_anchor: any) => {\n        (StateProvider as any)(_anchor, {\n          ...props,\n          children: (() => {\n            runTest();\n          }) as any,\n        });\n      }) as any,\n      { target: document.body },\n    );\n    unmount(c);\n  };\n}\n\ndescribe(\"StateProvider\", () => {\n  it(\n    \"provides initial state to consumers\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        expect(ctx.state).toEqual({ user: { name: \"John\" } });\n      },\n      { initialState: { user: { name: \"John\" } } },\n    ),\n  );\n\n  it(\n    \"provides empty object when no initial state\",\n    component(() => {\n      const ctx = getStateContext();\n      expect(ctx.state).toEqual({});\n    }),\n  );\n});\n\ndescribe(\"StateContext.get\", () => {\n  it(\n    \"retrieves values by path\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        expect(ctx.get(\"/user/name\")).toBe(\"John\");\n        expect(ctx.get(\"/user/age\")).toBe(30);\n      },\n      { initialState: { user: { name: \"John\", age: 30 } } },\n    ),\n  );\n\n  it(\n    \"returns undefined for missing path\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        expect(ctx.get(\"/user/email\")).toBeUndefined();\n        expect(ctx.get(\"/nonexistent\")).toBeUndefined();\n      },\n      { initialState: { user: { name: \"John\" } } },\n    ),\n  );\n});\n\ndescribe(\"StateContext.set\", () => {\n  it(\n    \"updates values at path\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        ctx.set(\"/count\", 5);\n        expect(ctx.state.count).toBe(5);\n      },\n      { initialState: { count: 0 } },\n    ),\n  );\n\n  it(\n    \"creates nested paths\",\n    component(() => {\n      const ctx = getStateContext();\n      ctx.set(\"/user/name\", \"Jane\");\n      expect(ctx.get(\"/user/name\")).toBe(\"Jane\");\n    }),\n  );\n\n  it(\n    \"calls onStateChange callback with change entries\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        ctx.set(\"/value\", 2);\n      },\n      {\n        initialState: { value: 1 },\n        onStateChange: vi.fn((changes) => {\n          expect(changes).toEqual([{ path: \"/value\", value: 2 }]);\n        }),\n      },\n    ),\n  );\n});\n\ndescribe(\"StateContext.update\", () => {\n  it(\n    \"handles multiple values at once\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        ctx.update({ \"/a\": 10, \"/b\": 20 });\n        expect(ctx.state.a).toBe(10);\n        expect(ctx.state.b).toBe(20);\n      },\n      { initialState: { a: 1, b: 2 } },\n    ),\n  );\n\n  it(\n    \"calls onStateChange once with all changed updates\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        ctx.update({ \"/x\": 1, \"/y\": 2 });\n      },\n      {\n        initialState: { x: 0, y: 0 },\n        onStateChange: vi.fn((changes) => {\n          expect(changes).toEqual([\n            { path: \"/x\", value: 1 },\n            { path: \"/y\", value: 2 },\n          ]);\n        }),\n      },\n    ),\n  );\n});\n\ndescribe(\"StateContext nested paths\", () => {\n  it(\n    \"handles deeply nested state paths\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        expect(ctx.get(\"/app/settings/theme\")).toBe(\"light\");\n        expect(ctx.get(\"/app/settings/notifications/enabled\")).toBe(true);\n        ctx.set(\"/app/settings/theme\", \"dark\");\n        expect(ctx.get(\"/app/settings/theme\")).toBe(\"dark\");\n      },\n      {\n        initialState: {\n          app: {\n            settings: {\n              theme: \"light\",\n              notifications: { enabled: true },\n            },\n          },\n        },\n      },\n    ),\n  );\n\n  it(\n    \"handles array indices in paths\",\n    component(\n      () => {\n        const ctx = getStateContext();\n        expect(ctx.get(\"/items/0\")).toBe(\"a\");\n        expect(ctx.get(\"/items/1\")).toBe(\"b\");\n        ctx.set(\"/items/1\", \"B\");\n        expect(ctx.get(\"/items/1\")).toBe(\"B\");\n      },\n      { initialState: { items: [\"a\", \"b\", \"c\"] } },\n    ),\n  );\n});\n\ndescribe(\"controlled mode\", () => {\n  it(\n    \"reads and writes through external StateStore\",\n    (() => {\n      const store = createStateStore({ count: 1 });\n      const onStateChange = vi.fn();\n      return component(\n        () => {\n          const ctx = getStateContext();\n          expect(ctx.get(\"/count\")).toBe(1);\n          ctx.set(\"/count\", 2);\n          expect(store.get(\"/count\")).toBe(2);\n          expect(onStateChange).not.toHaveBeenCalled();\n        },\n        { store, onStateChange },\n      );\n    })(),\n  );\n});\n"
  },
  {
    "path": "packages/svelte/src/contexts/visibility.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { mount, unmount } from \"svelte\";\nimport StateProvider, { getStateContext } from \"./StateProvider.svelte\";\nimport VisibilityProvider, {\n  getVisibilityContext,\n} from \"./VisibilityProvider.svelte\";\n\nfunction component(\n  runTest: () => void,\n  initialState: Record<string, unknown> = {},\n) {\n  return () => {\n    const c = mount(\n      ((_anchor: any) => {\n        (StateProvider as any)(_anchor, {\n          initialState,\n          children: ((_inner: any) => {\n            (VisibilityProvider as any)(_inner, {\n              children: (() => {\n                runTest();\n              }) as any,\n            });\n          }) as any,\n        });\n      }) as any,\n      { target: document.body },\n    );\n    unmount(c);\n  };\n}\n\ndescribe(\"VisibilityProvider\", () => {\n  it(\n    \"provides isVisible function\",\n    component(() => {\n      const visCtx = getVisibilityContext();\n\n      expect(typeof visCtx.isVisible).toBe(\"function\");\n    }),\n  );\n\n  it(\n    \"provides visibility context\",\n    component(\n      () => {\n        const visCtx = getVisibilityContext();\n\n        expect(visCtx.ctx).toBeDefined();\n        expect(visCtx.ctx.stateModel).toEqual({ value: true });\n      },\n      { value: true },\n    ),\n  );\n});\n\ndescribe(\"isVisible\", () => {\n  it(\n    \"returns true for undefined condition\",\n    component(() => {\n      const visCtx = getVisibilityContext();\n\n      expect(visCtx.isVisible(undefined)).toBe(true);\n    }),\n  );\n\n  it(\n    \"returns true for true condition\",\n    component(() => {\n      const visCtx = getVisibilityContext();\n\n      expect(visCtx.isVisible(true)).toBe(true);\n    }),\n  );\n\n  it(\n    \"returns false for false condition\",\n    component(() => {\n      const visCtx = getVisibilityContext();\n\n      expect(visCtx.isVisible(false)).toBe(false);\n    }),\n  );\n\n  it(\n    \"evaluates $state conditions against data\",\n    component(\n      () => {\n        const stateCtx = getStateContext();\n        const visCtx = getVisibilityContext();\n\n        expect(visCtx.isVisible({ $state: \"/isLoggedIn\" })).toBe(true);\n\n        stateCtx.set(\"/isLoggedIn\", false);\n\n        expect(visCtx.isVisible({ $state: \"/isLoggedIn\" })).toBe(false);\n      },\n      { isLoggedIn: true },\n    ),\n  );\n\n  it(\n    \"evaluates equality conditions\",\n    component(\n      () => {\n        const visCtx = getVisibilityContext();\n\n        expect(visCtx.isVisible({ $state: \"/tab\", eq: \"home\" })).toBe(true);\n        expect(visCtx.isVisible({ $state: \"/tab\", eq: \"settings\" })).toBe(\n          false,\n        );\n      },\n      { tab: \"home\" },\n    ),\n  );\n\n  it(\n    \"evaluates array conditions (implicit AND)\",\n    component(\n      () => {\n        const visCtx = getVisibilityContext();\n\n        expect(visCtx.isVisible([{ $state: \"/a\" }, { $state: \"/b\" }])).toBe(\n          true,\n        );\n\n        expect(visCtx.isVisible([{ $state: \"/a\" }, { $state: \"/c\" }])).toBe(\n          false,\n        );\n      },\n      { a: true, b: true, c: false },\n    ),\n  );\n\n  it(\n    \"evaluates $and conditions\",\n    component(\n      () => {\n        const visCtx = getVisibilityContext();\n\n        expect(\n          visCtx.isVisible({ $and: [{ $state: \"/x\" }, { $state: \"/y\" }] }),\n        ).toBe(false);\n      },\n      { x: true, y: false },\n    ),\n  );\n\n  it(\n    \"evaluates $or conditions\",\n    component(\n      () => {\n        const visCtx = getVisibilityContext();\n\n        expect(\n          visCtx.isVisible({ $or: [{ $state: \"/x\" }, { $state: \"/y\" }] }),\n        ).toBe(true);\n      },\n      { x: true, y: false },\n    ),\n  );\n});\n"
  },
  {
    "path": "packages/svelte/src/index.ts",
    "content": "// =============================================================================\n// Contexts\n// =============================================================================\n\nexport {\n  default as StateProvider,\n  getStateContext,\n  getStateValue,\n  getBoundProp,\n  type StateContext,\n} from \"./contexts/StateProvider.svelte\";\n\nexport {\n  default as VisibilityProvider,\n  getVisibilityContext,\n  isVisible,\n  type VisibilityContext,\n} from \"./contexts/VisibilityProvider.svelte\";\n\nexport {\n  default as ActionProvider,\n  getActionContext,\n  getAction,\n  type ActionContext,\n  type PendingConfirmation,\n} from \"./contexts/ActionProvider.svelte\";\n\nexport {\n  default as ValidationProvider,\n  getValidationContext,\n  getOptionalValidationContext,\n  getFieldValidation,\n  type ValidationContext,\n  type FieldValidationState,\n} from \"./contexts/ValidationProvider.svelte\";\n\nexport {\n  default as RepeatScopeProvider,\n  getRepeatScope,\n  type RepeatScopeValue,\n} from \"./contexts/RepeatScopeProvider.svelte\";\n\nexport {\n  default as FunctionsContextProvider,\n  getFunctions,\n  type FunctionsContext,\n} from \"./contexts/FunctionsContextProvider.svelte\";\n\n// =============================================================================\n// Schema\n// =============================================================================\n\nexport { schema, type SvelteSchema, type SvelteSpec } from \"./schema.js\";\n\n// =============================================================================\n// Catalog Types\n// =============================================================================\n\nexport type {\n  EventHandle,\n  BaseComponentProps,\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n  ActionFn,\n  Actions,\n} from \"./catalog-types.js\";\n\n// =============================================================================\n// Utilities\n// =============================================================================\n\nexport {\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n  type DataPart,\n} from \"./utils.svelte.js\";\n\n// =============================================================================\n// Streaming\n// =============================================================================\n\nexport {\n  createUIStream,\n  createChatUI,\n  type UIStreamOptions,\n  type UIStreamReturn,\n  type UIStreamState,\n  type ChatUIOptions,\n  type ChatUIReturn,\n  type ChatMessage,\n  type TokenUsage,\n} from \"./streaming.svelte.js\";\n\n// =============================================================================\n// Registry\n// =============================================================================\n\nexport {\n  defineRegistry,\n  createRenderer,\n  type DefineRegistryResult,\n  type ComponentRenderer,\n  type ComponentRegistry,\n} from \"./renderer.js\";\nexport { default as Renderer, type RendererProps } from \"./Renderer.svelte\";\nexport {\n  default as CatalogRenderer,\n  type CatalogRendererProps,\n} from \"./CatalogRenderer.svelte\";\nexport {\n  default as JsonUIProvider,\n  type JSONUIProviderProps,\n} from \"./JsonUIProvider.svelte\";\nexport { default as ConfirmDialog } from \"./ConfirmDialog.svelte\";\nexport { default as ConfirmDialogManager } from \"./ConfirmDialogManager.svelte\";\n\n// =============================================================================\n// Re-exports from core\n// =============================================================================\n\nexport type {\n  Spec,\n  UIElement,\n  ActionBinding,\n  ActionHandler,\n} from \"@json-render/core\";\n"
  },
  {
    "path": "packages/svelte/src/renderer.test.ts",
    "content": "import { describe, it, expect, afterEach } from \"vitest\";\nimport { render, cleanup } from \"@testing-library/svelte\";\nimport type { Spec } from \"@json-render/core\";\nimport RendererWithProvider from \"./RendererWithProvider.test.svelte\";\nimport TestContainer from \"./TestContainer.svelte\";\nimport TestText from \"./TestText.svelte\";\nimport TestButton from \"./TestButton.svelte\";\nimport { defineRegistry } from \"./renderer.js\";\n\ndescribe(\"Renderer\", () => {\n  afterEach(() => {\n    cleanup();\n  });\n\n  const { registry } = defineRegistry(null as any, {\n    components: {\n      Container: TestContainer,\n      Text: TestText,\n      Button: TestButton,\n    },\n  });\n\n  function mountRenderer(\n    spec: Spec | null,\n    options: { loading?: boolean } = {},\n  ) {\n    return render(RendererWithProvider, {\n      props: {\n        spec,\n        registry,\n        loading: options.loading ?? false,\n        initialState: spec?.state ?? {},\n      },\n    });\n  }\n\n  it(\"renders nothing for null spec\", () => {\n    const { container } = mountRenderer(null);\n\n    // Should have no content rendered from Renderer\n    expect(container.querySelector(\".test-container\")).toBeNull();\n    expect(container.querySelector(\".test-text\")).toBeNull();\n  });\n\n  it(\"renders nothing for spec with empty root\", () => {\n    const spec: Spec = { root: \"\", elements: {} };\n    const { container } = mountRenderer(spec);\n\n    expect(container.querySelector(\".test-container\")).toBeNull();\n  });\n\n  it(\"renders a single element\", () => {\n    const spec: Spec = {\n      root: \"text1\",\n      elements: {\n        text1: {\n          type: \"Text\",\n          props: { text: \"Hello World\" },\n          children: [],\n        },\n      },\n    };\n    const { container } = mountRenderer(spec);\n\n    const textEl = container.querySelector(\".test-text\");\n    expect(textEl).not.toBeNull();\n    expect(textEl?.textContent).toBe(\"Hello World\");\n  });\n\n  it(\"renders nested elements\", () => {\n    const spec: Spec = {\n      root: \"container\",\n      elements: {\n        container: {\n          type: \"Container\",\n          props: { title: \"My Container\" },\n          children: [\"text1\", \"text2\"],\n        },\n        text1: {\n          type: \"Text\",\n          props: { text: \"First\" },\n          children: [],\n        },\n        text2: {\n          type: \"Text\",\n          props: { text: \"Second\" },\n          children: [],\n        },\n      },\n    };\n    const { container } = mountRenderer(spec);\n\n    const containerEl = container.querySelector(\".test-container\");\n    expect(containerEl).not.toBeNull();\n    expect(containerEl?.querySelector(\"h2\")?.textContent).toBe(\"My Container\");\n\n    const texts = container.querySelectorAll(\".test-text\");\n    expect(texts).toHaveLength(2);\n    expect(texts[0]?.textContent).toBe(\"First\");\n    expect(texts[1]?.textContent).toBe(\"Second\");\n  });\n\n  it(\"renders deeply nested elements\", () => {\n    const spec: Spec = {\n      root: \"outer\",\n      elements: {\n        outer: {\n          type: \"Container\",\n          props: { title: \"Outer\" },\n          children: [\"inner\"],\n        },\n        inner: {\n          type: \"Container\",\n          props: { title: \"Inner\" },\n          children: [\"text\"],\n        },\n        text: {\n          type: \"Text\",\n          props: { text: \"Deep text\" },\n          children: [],\n        },\n      },\n    };\n    const { container } = mountRenderer(spec);\n\n    const containers = container.querySelectorAll(\".test-container\");\n    expect(containers).toHaveLength(2);\n\n    const text = container.querySelector(\".test-text\");\n    expect(text?.textContent).toBe(\"Deep text\");\n  });\n\n  it(\"passes loading prop to components\", () => {\n    const spec: Spec = {\n      root: \"container\",\n      elements: {\n        container: {\n          type: \"Container\",\n          props: {},\n          children: [],\n        },\n      },\n    };\n    const { container } = mountRenderer(spec, { loading: true });\n\n    const containerEl = container.querySelector(\".test-container\");\n    expect(containerEl?.getAttribute(\"data-loading\")).toBe(\"true\");\n  });\n\n  it(\"renders nothing for unknown component types without fallback\", () => {\n    const spec: Spec = {\n      root: \"unknown\",\n      elements: {\n        unknown: {\n          type: \"UnknownType\",\n          props: {},\n          children: [],\n        },\n      },\n    };\n    const { container } = mountRenderer(spec);\n\n    // No elements should be rendered for unknown type\n    expect(container.querySelector(\".test-container\")).toBeNull();\n    expect(container.querySelector(\".test-text\")).toBeNull();\n  });\n\n  it(\"skips missing child elements gracefully\", () => {\n    const spec: Spec = {\n      root: \"container\",\n      elements: {\n        container: {\n          type: \"Container\",\n          props: { title: \"Parent\" },\n          children: [\"existing\", \"missing\"],\n        },\n        existing: {\n          type: \"Text\",\n          props: { text: \"I exist\" },\n          children: [],\n        },\n        // \"missing\" element is not defined\n      },\n    };\n    const { container } = mountRenderer(spec);\n\n    const containerEl = container.querySelector(\".test-container\");\n    expect(containerEl).not.toBeNull();\n\n    const texts = container.querySelectorAll(\".test-text\");\n    expect(texts).toHaveLength(1);\n    expect(texts[0]?.textContent).toBe(\"I exist\");\n  });\n});\n"
  },
  {
    "path": "packages/svelte/src/renderer.ts",
    "content": "import type {\n  Catalog,\n  ComputedFunction,\n  SchemaDefinition,\n  Spec,\n  StateStore,\n  UIElement,\n} from \"@json-render/core\";\nimport type { Component, Snippet } from \"svelte\";\nimport type {\n  BaseComponentProps,\n  EventHandle,\n  SetState,\n  StateModel,\n} from \"./catalog-types.js\";\nimport CatalogRenderer from \"./CatalogRenderer.svelte\";\n\n/**\n * Props passed to component renderers\n */\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  /** The element being rendered */\n  element: UIElement<string, P>;\n  /** Rendered children snippet */\n  children?: Snippet;\n  /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  /** Whether the parent is loading */\n  loading?: boolean;\n}\n\n/**\n * Component renderer type - a Svelte component that receives ComponentRenderProps\n */\nexport type ComponentRenderer<P = Record<string, unknown>> = Component<\n  ComponentRenderProps<P>\n>;\n\n/**\n * Registry of component renderers.\n * Maps component type names to Svelte components.\n */\nexport type ComponentRegistry = Record<string, ComponentRenderer<any>>;\n\n/**\n * Action handler function for defineRegistry\n */\ntype DefineRegistryActionFn = (\n  params: Record<string, unknown> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Result returned by defineRegistry\n */\nexport interface DefineRegistryResult {\n  /** Component registry for Renderer */\n  registry: ComponentRegistry;\n  /**\n   * Create ActionProvider-compatible handlers.\n   */\n  handlers: (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;\n  /**\n   * Execute an action by name imperatively\n   */\n  executeAction: (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state?: StateModel,\n  ) => Promise<void>;\n}\n\n/**\n * Create a registry from a catalog with Svelte components and/or actions.\n *\n * Components must accept `BaseComponentProps` as their props interface.\n *\n * @example\n * ```ts\n * import { defineRegistry } from \"@json-render/svelte\";\n * import Card from \"./components/Card.svelte\";\n * import Button from \"./components/Button.svelte\";\n * import { myCatalog } from \"./catalog\";\n *\n * const { registry, handlers } = defineRegistry(myCatalog, {\n *   components: {\n *     Card,\n *     Button,\n *   },\n *   actions: {\n *     submit: async (params, setState) => {\n *       // handle action\n *     },\n *   },\n * });\n * ```\n */\nexport function defineRegistry<\n  C extends Catalog,\n  TComponents extends Record<string, Component<BaseComponentProps<any>>>,\n>(\n  _catalog: C,\n  options: {\n    /** Svelte components that accept BaseComponentProps */\n    components?: TComponents;\n    /** Action handlers */\n    actions?: Record<string, DefineRegistryActionFn>;\n  },\n): DefineRegistryResult {\n  const registry: ComponentRegistry = {};\n\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = (_, props) =>\n        (componentFn as Component<BaseComponentProps<any>>)(_, {\n          get props() {\n            return props.element.props;\n          },\n          get children() {\n            return props.children;\n          },\n          get emit() {\n            return props.emit;\n          },\n          get on() {\n            return props.on;\n          },\n          get bindings() {\n            return props.bindings;\n          },\n          get loading() {\n            return props.loading;\n          },\n        });\n    }\n  }\n\n  // Build action helpers\n  const actionMap = options.actions\n    ? (Object.entries(options.actions) as Array<\n        [string, DefineRegistryActionFn]\n      >)\n    : [];\n\n  const handlers = (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {\n    const result: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void>\n    > = {};\n    for (const [name, actionFn] of actionMap) {\n      result[name] = async (params) => {\n        const setState = getSetState();\n        const state = getState();\n        if (setState) {\n          await actionFn(params, setState, state);\n        }\n      };\n    }\n    return result;\n  };\n\n  const executeAction = async (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state: StateModel = {},\n  ): Promise<void> => {\n    const entry = actionMap.find(([name]) => name === actionName);\n    if (entry) {\n      await entry[1](params, setState, state);\n    } else {\n      console.warn(`Unknown action: ${actionName}`);\n    }\n  };\n\n  return { registry, handlers, executeAction };\n}\n\n// ============================================================================\n// createRenderer\n// ============================================================================\n\n/**\n * Props for renderers created with createRenderer\n */\nexport interface CreateRendererProps {\n  spec: Spec | null;\n  store?: StateStore;\n  state?: Record<string, unknown>;\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  loading?: boolean;\n  fallback?: Component;\n}\n\n/**\n * Component map type — maps component names to Svelte components\n */\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: Component<any, any, string>;\n};\n\n/**\n * Create a renderer from a catalog\n *\n * @example\n * ```typescript\n * const DashboardRenderer = createRenderer(dashboardCatalog, {\n *   Card,\n *   Metric,\n * });\n *\n * // Usage in template\n * <DashboardRenderer spec={aiGeneratedSpec} {state} />\n * ```\n */\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  _catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): Component<CreateRendererProps> {\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  return (_, props: CreateRendererProps) =>\n    CatalogRenderer(_, {\n      registry,\n      get spec() {\n        return props.spec;\n      },\n      get store() {\n        return props.store;\n      },\n      get state() {\n        return props.state;\n      },\n      get onAction() {\n        return props.onAction;\n      },\n      get onStateChange() {\n        return props.onStateChange;\n      },\n      get functions() {\n        return props.functions;\n      },\n      get loading() {\n        return props.loading;\n      },\n      get fallback() {\n        return props.fallback;\n      },\n    });\n}\n"
  },
  {
    "path": "packages/svelte/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/svelte\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like\n    spec: s.object({\n      /** Root element key */\n      root: s.string(),\n      /** Flat map of elements by key */\n      elements: s.record(\n        s.object({\n          /** Component type from catalog */\n          type: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Child element keys (flat reference) */\n          children: s.array(s.string()),\n          /** Visibility condition */\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Component definitions */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n        slots: s.array(s.string()),\n        /** Description for AI generation hints */\n        description: s.string(),\n        /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n        example: s.any(),\n      }),\n      /** Action definitions (optional) */\n      actions: s.map({\n        /** Zod schema for action params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    builtInActions: [\n      {\n        name: \"setState\",\n        description:\n          \"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }\",\n      },\n      {\n        name: \"pushState\",\n        description:\n          'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {\"$state\":\"/path\"} refs and \"$id\" for auto IDs.',\n      },\n      {\n        name: \"removeState\",\n        description:\n          \"Remove an item from an array in state by index. Params: { statePath: string, index: number }\",\n      },\n      {\n        name: \"validateForm\",\n        description:\n          \"Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.\",\n      },\n    ],\n    defaultRules: [\n      // Element integrity\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n      \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n\n      // Field placement\n      'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"},\"children\":[...]}.',\n      'CRITICAL: The \"on\" field goes on the ELEMENT object, NOT inside \"props\". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',\n\n      // State and data\n      \"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).\",\n      'When building repeating content backed by a state array (e.g. posts, products, items), use the \"repeat\" field on a container element. Example: { \"type\": \"<ContainerComponent>\", \"props\": {}, \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" }, \"children\": [\"post-card\"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n      // Design quality\n      \"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.\",\n    ],\n  },\n);\n\n/**\n * Type for the Svelte schema\n */\nexport type SvelteSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type SvelteSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/svelte/src/streaming.svelte.ts",
    "content": "import type { Spec, JsonPatch } from \"@json-render/core\";\nimport {\n  setByPath,\n  getByPath,\n  removeByPath,\n  createMixedStreamParser,\n  applySpecPatch,\n} from \"@json-render/core\";\n\n/**\n * Token usage metadata from AI generation\n */\nexport interface TokenUsage {\n  promptTokens: number;\n  completionTokens: number;\n  totalTokens: number;\n}\n\n/**\n * UI Stream state\n */\nexport interface UIStreamState {\n  spec: Spec | null;\n  isStreaming: boolean;\n  error: Error | null;\n  usage: TokenUsage | null;\n  rawLines: string[];\n}\n\n/**\n * UI Stream return type\n */\nexport interface UIStreamReturn {\n  readonly spec: Spec | null;\n  readonly isStreaming: boolean;\n  readonly error: Error | null;\n  readonly usage: TokenUsage | null;\n  readonly rawLines: string[];\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  clear: () => void;\n}\n\n/**\n * Options for createUIStream\n */\nexport interface UIStreamOptions {\n  api: string;\n  onComplete?: (spec: Spec) => void;\n  onError?: (error: Error) => void;\n}\n\ntype ParsedLine =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"usage\"; usage: TokenUsage }\n  | null;\n\nfunction parseLine(line: string): ParsedLine {\n  try {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"//\")) {\n      return null;\n    }\n    const parsed = JSON.parse(trimmed);\n\n    if (parsed.__meta === \"usage\") {\n      return {\n        type: \"usage\",\n        usage: {\n          promptTokens: parsed.promptTokens ?? 0,\n          completionTokens: parsed.completionTokens ?? 0,\n          totalTokens: parsed.totalTokens ?? 0,\n        },\n      };\n    }\n\n    return { type: \"patch\", patch: parsed as JsonPatch };\n  } catch {\n    return null;\n  }\n}\n\nfunction setSpecValue(newSpec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    const statePath = path.slice(\"/state\".length);\n    setByPath(newSpec.state as Record<string, unknown>, statePath, value);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      newSpec.elements[elementKey] = value as Spec[\"elements\"][string];\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\nfunction removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    delete newSpec.state;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    const statePath = path.slice(\"/state\".length);\n    removeByPath(newSpec.state as Record<string, unknown>, statePath);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      const { [elementKey]: _, ...rest } = newSpec.elements;\n      newSpec.elements = rest;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\nfunction getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  if (path === \"/state\") return spec.state;\n  if (path.startsWith(\"/state/\") && spec.state) {\n    const statePath = path.slice(\"/state\".length);\n    return getByPath(spec.state as Record<string, unknown>, statePath);\n  }\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\": {\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    }\n    case \"remove\": {\n      removeSpecValue(newSpec, patch.path);\n      break;\n    }\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getSpecValue(newSpec, patch.from);\n      removeSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      break;\n    }\n  }\n\n  return newSpec;\n}\n\n/**\n * Create a streaming UI generator using Svelte 5 $state\n */\nexport function createUIStream({\n  api,\n  onComplete,\n  onError,\n}: UIStreamOptions): UIStreamReturn {\n  let spec = $state<Spec | null>(null);\n  let isStreaming = $state(false);\n  let error = $state<Error | null>(null);\n  let usage = $state<TokenUsage | null>(null);\n  let rawLines = $state<string[]>([]);\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    spec = null;\n    error = null;\n    usage = null;\n    rawLines = [];\n  };\n\n  const send = async (\n    prompt: string,\n    context?: Record<string, unknown>,\n  ): Promise<void> => {\n    abortController?.abort();\n    abortController = new AbortController();\n\n    isStreaming = true;\n    error = null;\n    usage = null;\n    rawLines = [];\n\n    const previousSpec = context?.previousSpec as Spec | undefined;\n    let currentSpec: Spec =\n      previousSpec && previousSpec.root\n        ? { ...previousSpec, elements: { ...previousSpec.elements } }\n        : { root: \"\", elements: {} };\n    spec = currentSpec;\n\n    try {\n      const response = await fetch(api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ prompt, context, currentSpec }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) errorMessage = errorData.message;\n          else if (errorData.error) errorMessage = errorData.error;\n        } catch {\n          // Ignore\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n      let buffer = \"\";\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        buffer += decoder.decode(value, { stream: true });\n        const lines = buffer.split(\"\\n\");\n        buffer = lines.pop() ?? \"\";\n\n        for (const line of lines) {\n          const trimmed = line.trim();\n          if (!trimmed) continue;\n          const result = parseLine(trimmed);\n          if (!result) continue;\n          if (result.type === \"usage\") {\n            usage = result.usage;\n          } else {\n            rawLines = [...rawLines, trimmed];\n            currentSpec = applyPatch(currentSpec, result.patch);\n            spec = { ...currentSpec };\n          }\n        }\n      }\n\n      if (buffer.trim()) {\n        const trimmed = buffer.trim();\n        const result = parseLine(trimmed);\n        if (result) {\n          if (result.type === \"usage\") {\n            usage = result.usage;\n          } else {\n            rawLines = [...rawLines, trimmed];\n            currentSpec = applyPatch(currentSpec, result.patch);\n            spec = { ...currentSpec };\n          }\n        }\n      }\n\n      onComplete?.(currentSpec);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") return;\n      const e = err instanceof Error ? err : new Error(String(err));\n      error = e;\n      onError?.(e);\n    } finally {\n      isStreaming = false;\n    }\n  };\n\n  return {\n    get spec() {\n      return spec;\n    },\n    get isStreaming() {\n      return isStreaming;\n    },\n    get error() {\n      return error;\n    },\n    get usage() {\n      return usage;\n    },\n    get rawLines() {\n      return rawLines;\n    },\n    send,\n    clear,\n  };\n}\n\n/**\n * Chat message type\n */\nexport interface ChatMessage {\n  id: string;\n  role: \"user\" | \"assistant\";\n  text: string;\n  spec: Spec | null;\n}\n\n/**\n * Chat UI options\n */\nexport interface ChatUIOptions {\n  api: string;\n  onComplete?: (message: ChatMessage) => void;\n  onError?: (error: Error) => void;\n}\n\n/**\n * Chat UI return type\n */\nexport interface ChatUIReturn {\n  readonly messages: ChatMessage[];\n  readonly isStreaming: boolean;\n  readonly error: Error | null;\n  send: (text: string) => Promise<void>;\n  clear: () => void;\n}\n\nlet chatMessageIdCounter = 0;\nfunction generateChatId(): string {\n  if (\n    typeof crypto !== \"undefined\" &&\n    typeof crypto.randomUUID === \"function\"\n  ) {\n    return crypto.randomUUID();\n  }\n  chatMessageIdCounter += 1;\n  return `msg-${Date.now()}-${chatMessageIdCounter}`;\n}\n\n/**\n * Create a chat UI with streaming support\n */\nexport function createChatUI({\n  api,\n  onComplete,\n  onError,\n}: ChatUIOptions): ChatUIReturn {\n  let messages = $state<ChatMessage[]>([]);\n  let isStreaming = $state(false);\n  let error = $state<Error | null>(null);\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    messages = [];\n    error = null;\n  };\n\n  const send = async (text: string): Promise<void> => {\n    if (!text.trim()) return;\n\n    abortController?.abort();\n    abortController = new AbortController();\n\n    const userMessage: ChatMessage = {\n      id: generateChatId(),\n      role: \"user\",\n      text: text.trim(),\n      spec: null,\n    };\n\n    const assistantId = generateChatId();\n    const assistantMessage: ChatMessage = {\n      id: assistantId,\n      role: \"assistant\",\n      text: \"\",\n      spec: null,\n    };\n\n    messages = [...messages, userMessage, assistantMessage];\n    isStreaming = true;\n    error = null;\n\n    const historyForApi = messages\n      .slice(0, -1)\n      .map((m) => ({ role: m.role, content: m.text }));\n\n    let accumulatedText = \"\";\n    let currentSpec: Spec = { root: \"\", elements: {} };\n    let hasSpec = false;\n\n    try {\n      const response = await fetch(api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ messages: historyForApi }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) errorMessage = errorData.message;\n          else if (errorData.error) errorMessage = errorData.error;\n        } catch {\n          // Ignore\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) throw new Error(\"No response body\");\n\n      const decoder = new TextDecoder();\n\n      const parser = createMixedStreamParser({\n        onPatch(patch) {\n          hasSpec = true;\n          applySpecPatch(currentSpec, patch);\n          messages = messages.map((m) =>\n            m.id === assistantId\n              ? {\n                  ...m,\n                  spec: {\n                    root: currentSpec.root,\n                    elements: { ...currentSpec.elements },\n                    ...(currentSpec.state\n                      ? { state: { ...currentSpec.state } }\n                      : {}),\n                  },\n                }\n              : m,\n          );\n        },\n        onText(line) {\n          accumulatedText += (accumulatedText ? \"\\n\" : \"\") + line;\n          messages = messages.map((m) =>\n            m.id === assistantId ? { ...m, text: accumulatedText } : m,\n          );\n        },\n      });\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        parser.push(decoder.decode(value, { stream: true }));\n      }\n      parser.flush();\n\n      const finalMessage: ChatMessage = {\n        id: assistantId,\n        role: \"assistant\",\n        text: accumulatedText,\n        spec: hasSpec\n          ? {\n              root: currentSpec.root,\n              elements: { ...currentSpec.elements },\n              ...(currentSpec.state ? { state: { ...currentSpec.state } } : {}),\n            }\n          : null,\n      };\n      onComplete?.(finalMessage);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") return;\n      const e = err instanceof Error ? err : new Error(String(err));\n      error = e;\n      messages = messages.filter(\n        (m) => m.id !== assistantId || m.text.length > 0,\n      );\n      onError?.(e);\n    } finally {\n      isStreaming = false;\n    }\n  };\n\n  return {\n    get messages() {\n      return messages;\n    },\n    get isStreaming() {\n      return isStreaming;\n    },\n    get error() {\n      return error;\n    },\n    send,\n    clear,\n  };\n}\n"
  },
  {
    "path": "packages/svelte/src/utils.svelte.ts",
    "content": "import type {\n  Spec,\n  UIElement,\n  FlatElement,\n  SpecDataPart,\n} from \"@json-render/core\";\nimport {\n  applySpecPatch,\n  nestedToFlat,\n  SPEC_DATA_PART_TYPE,\n} from \"@json-render/core\";\n\n/**\n * A single part from an AI response. Minimal structural type for library helpers.\n */\nexport interface DataPart {\n  type: string;\n  text?: string;\n  data?: unknown;\n}\n\n/**\n * Convert a flat element list to a Spec.\n * Input elements use key/parentKey to establish identity and relationships.\n * Output spec uses the map-based format where key is the map entry key\n * and parent-child relationships are expressed through children arrays.\n */\nexport function flatToTree(elements: FlatElement[]): Spec {\n  const elementMap: Record<string, UIElement> = {};\n  let root = \"\";\n\n  // First pass: add all elements to map\n  for (const element of elements) {\n    elementMap[element.key] = {\n      type: element.type,\n      props: element.props,\n      children: [],\n      visible: element.visible,\n    };\n  }\n\n  // Second pass: build parent-child relationships\n  for (const element of elements) {\n    if (element.parentKey) {\n      const parent = elementMap[element.parentKey];\n      if (parent) {\n        if (!parent.children) {\n          parent.children = [];\n        }\n        parent.children.push(element.key);\n      }\n    } else {\n      root = element.key;\n    }\n  }\n\n  return { root, elements: elementMap };\n}\n\n/**\n * Type guard that validates a data part payload looks like a valid SpecDataPart.\n */\nfunction isSpecDataPart(data: unknown): data is SpecDataPart {\n  if (typeof data !== \"object\" || data === null) return false;\n  const obj = data as Record<string, unknown>;\n  switch (obj.type) {\n    case \"patch\":\n      return typeof obj.patch === \"object\" && obj.patch !== null;\n    case \"flat\":\n    case \"nested\":\n      return typeof obj.spec === \"object\" && obj.spec !== null;\n    default:\n      return false;\n  }\n}\n\n/**\n * Build a `Spec` by replaying all spec data parts from a message's parts array.\n * Returns `null` if no spec data parts are present.\n */\nexport function buildSpecFromParts(\n  parts: DataPart[],\n  snapshot = true,\n): Spec | null {\n  const spec: Spec = { root: \"\", elements: {} };\n  let hasSpec = false;\n\n  for (const part of parts) {\n    if (part.type === SPEC_DATA_PART_TYPE) {\n      if (!isSpecDataPart(part.data)) continue;\n      const payload = part.data;\n      if (payload.type === \"patch\") {\n        hasSpec = true;\n        applySpecPatch(\n          spec,\n          snapshot ? $state.snapshot(payload.patch) : payload.patch,\n        );\n      } else if (payload.type === \"flat\") {\n        hasSpec = true;\n        Object.assign(spec, payload.spec);\n      } else if (payload.type === \"nested\") {\n        hasSpec = true;\n        const flat = nestedToFlat(payload.spec);\n        Object.assign(spec, flat);\n      }\n    }\n  }\n\n  return hasSpec ? spec : null;\n}\n\n/**\n * Extract and join all text content from a message's parts array.\n */\nexport function getTextFromParts(parts: DataPart[]): string {\n  return parts\n    .filter(\n      (p): p is DataPart & { text: string } =>\n        p.type === \"text\" && typeof p.text === \"string\",\n    )\n    .map((p) => p.text.trim())\n    .filter(Boolean)\n    .join(\"\\n\\n\");\n}\n"
  },
  {
    "path": "packages/svelte/src/utils.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport {\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n} from \"./utils.svelte.js\";\nimport type { FlatElement, SpecDataPart } from \"@json-render/core\";\nimport { SPEC_DATA_PART_TYPE } from \"@json-render/core\";\n\ndescribe(\"flatToTree\", () => {\n  it(\"converts array of elements to tree structure\", () => {\n    const elements: FlatElement[] = [\n      { key: \"root\", type: \"Container\", props: {}, parentKey: undefined },\n      {\n        key: \"child1\",\n        type: \"Text\",\n        props: { text: \"Hello\" },\n        parentKey: \"root\",\n      },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.root).toBe(\"root\");\n    expect(spec.elements[\"root\"]).toBeDefined();\n    expect(spec.elements[\"child1\"]).toBeDefined();\n  });\n\n  it(\"builds parent-child relationships\", () => {\n    const elements: FlatElement[] = [\n      { key: \"root\", type: \"Container\", props: {}, parentKey: undefined },\n      { key: \"child1\", type: \"Text\", props: {}, parentKey: \"root\" },\n      { key: \"child2\", type: \"Text\", props: {}, parentKey: \"root\" },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.elements[\"root\"]?.children).toEqual([\"child1\", \"child2\"]);\n  });\n\n  it(\"handles single root element\", () => {\n    const elements: FlatElement[] = [\n      {\n        key: \"only\",\n        type: \"Text\",\n        props: { text: \"Solo\" },\n        parentKey: undefined,\n      },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.root).toBe(\"only\");\n    expect(spec.elements[\"only\"]?.children).toEqual([]);\n  });\n\n  it(\"handles deeply nested elements\", () => {\n    const elements: FlatElement[] = [\n      { key: \"root\", type: \"Container\", props: {}, parentKey: undefined },\n      { key: \"level1\", type: \"Container\", props: {}, parentKey: \"root\" },\n      { key: \"level2\", type: \"Container\", props: {}, parentKey: \"level1\" },\n      { key: \"level3\", type: \"Text\", props: {}, parentKey: \"level2\" },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.elements[\"root\"]?.children).toEqual([\"level1\"]);\n    expect(spec.elements[\"level1\"]?.children).toEqual([\"level2\"]);\n    expect(spec.elements[\"level2\"]?.children).toEqual([\"level3\"]);\n    expect(spec.elements[\"level3\"]?.children).toEqual([]);\n  });\n\n  it(\"preserves element props\", () => {\n    const elements: FlatElement[] = [\n      {\n        key: \"root\",\n        type: \"Card\",\n        props: { title: \"Hello\", value: 42 },\n        parentKey: undefined,\n      },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.elements[\"root\"]?.props).toEqual({ title: \"Hello\", value: 42 });\n  });\n\n  it(\"preserves visibility conditions\", () => {\n    const elements: FlatElement[] = [\n      {\n        key: \"root\",\n        type: \"Container\",\n        props: {},\n        parentKey: undefined,\n        visible: { $state: \"/isVisible\" },\n      },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.elements[\"root\"]?.visible).toEqual({ $state: \"/isVisible\" });\n  });\n\n  it(\"handles elements with undefined parentKey as root\", () => {\n    const elements: FlatElement[] = [\n      { key: \"a\", type: \"Text\", props: {}, parentKey: undefined },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.root).toBe(\"a\");\n  });\n\n  it(\"handles empty elements array\", () => {\n    const spec = flatToTree([]);\n\n    expect(spec.root).toBe(\"\");\n    expect(spec.elements).toEqual({});\n  });\n\n  it(\"handles multiple children correctly\", () => {\n    const elements: FlatElement[] = [\n      { key: \"root\", type: \"Container\", props: {}, parentKey: undefined },\n      { key: \"a\", type: \"Text\", props: {}, parentKey: \"root\" },\n      { key: \"b\", type: \"Text\", props: {}, parentKey: \"root\" },\n      { key: \"c\", type: \"Text\", props: {}, parentKey: \"root\" },\n    ];\n\n    const spec = flatToTree(elements);\n\n    expect(spec.elements[\"root\"]?.children).toHaveLength(3);\n    expect(spec.elements[\"root\"]?.children).toContain(\"a\");\n    expect(spec.elements[\"root\"]?.children).toContain(\"b\");\n    expect(spec.elements[\"root\"]?.children).toContain(\"c\");\n  });\n});\n\ndescribe(\"buildSpecFromParts\", () => {\n  it(\"returns null when no data-spec parts are present\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello world\" },\n      { type: \"other\", data: {} },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec).toBeNull();\n  });\n\n  it(\"builds a spec from patch parts\", () => {\n    const parts = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"main\" },\n        } satisfies SpecDataPart,\n      },\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/main\",\n            value: { type: \"Text\", props: { text: \"Hi\" }, children: [] },\n          },\n        } satisfies SpecDataPart,\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec).not.toBeNull();\n    expect(spec?.root).toBe(\"main\");\n    expect(spec?.elements[\"main\"]?.type).toBe(\"Text\");\n  });\n\n  it(\"handles flat spec parts\", () => {\n    const parts = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"root\",\n            elements: {\n              root: { type: \"Container\", props: {}, children: [] },\n            },\n          },\n        } satisfies SpecDataPart,\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec?.root).toBe(\"root\");\n    expect(spec?.elements[\"root\"]?.type).toBe(\"Container\");\n  });\n\n  it(\"ignores non-spec parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Some text\" },\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"r\",\n            elements: { r: { type: \"Text\", props: {}, children: [] } },\n          },\n        } satisfies SpecDataPart,\n      },\n      { type: \"tool-call\", data: {} },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec?.root).toBe(\"r\");\n  });\n\n  it(\"applies patches incrementally\", () => {\n    const parts = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"a\" },\n        } satisfies SpecDataPart,\n      },\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: {\n            op: \"add\",\n            path: \"/elements/a\",\n            value: { type: \"Text\", props: { n: 1 }, children: [] },\n          },\n        } satisfies SpecDataPart,\n      },\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: { op: \"replace\", path: \"/elements/a/props/n\", value: 2 },\n        } satisfies SpecDataPart,\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect((spec?.elements[\"a\"]?.props as { n: number }).n).toBe(2);\n  });\n\n  it(\"handles nested spec parts via nestedToFlat\", () => {\n    const parts = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"nested\",\n          spec: {\n            type: \"Container\",\n            props: {},\n            children: [{ type: \"Text\", props: { t: \"x\" } }],\n          },\n        } satisfies SpecDataPart,\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec).not.toBeNull();\n    expect(Object.keys(spec?.elements ?? {}).length).toBeGreaterThan(0);\n  });\n\n  it(\"handles mixed patch + flat + nested parts in sequence\", () => {\n    const parts = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"initial\" },\n        } satisfies SpecDataPart,\n      },\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"flat\",\n          spec: {\n            root: \"replaced\",\n            elements: { replaced: { type: \"Box\", props: {}, children: [] } },\n          },\n        } satisfies SpecDataPart,\n      },\n    ];\n\n    const spec = buildSpecFromParts(parts);\n\n    expect(spec?.root).toBe(\"replaced\");\n  });\n\n  it(\"returns empty elements map from empty parts list\", () => {\n    const spec = buildSpecFromParts([]);\n\n    expect(spec).toBeNull();\n  });\n});\n\ndescribe(\"getTextFromParts\", () => {\n  it(\"extracts text from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"World\" },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"returns empty string when no text parts\", () => {\n    const parts = [\n      { type: \"data\", data: {} },\n      { type: \"tool-call\", data: {} },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"\");\n  });\n\n  it(\"ignores non-text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Keep\" },\n      { type: \"data\", data: {} },\n      { type: \"text\", text: \"This\" },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"Keep\\n\\nThis\");\n  });\n\n  it(\"trims whitespace from text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"  Hello  \" },\n      { type: \"text\", text: \"\\n\\nWorld\\n\\n\" },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"skips empty text parts\", () => {\n    const parts = [\n      { type: \"text\", text: \"Hello\" },\n      { type: \"text\", text: \"   \" },\n      { type: \"text\", text: \"World\" },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"Hello\\n\\nWorld\");\n  });\n\n  it(\"ignores text parts with non-string text field\", () => {\n    const parts = [\n      { type: \"text\", text: \"Valid\" },\n      { type: \"text\", text: 123 as unknown as string },\n      { type: \"text\", text: \"Also Valid\" },\n    ];\n\n    const text = getTextFromParts(parts);\n\n    expect(text).toBe(\"Valid\\n\\nAlso Valid\");\n  });\n});\n"
  },
  {
    "path": "packages/svelte/svelte.config.js",
    "content": "/** @type {import('@sveltejs/package').Config} */\nexport default {\n  compilerOptions: {\n    runes: true, // ensure no legacy syntax sneaks into this code base\n  },\n};\n"
  },
  {
    "path": "packages/svelte/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"types\": [\"svelte\"],\n    \"verbatimModuleSyntax\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\", \"./**/*.test.ts\"]\n}\n"
  },
  {
    "path": "packages/typescript-config/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"esModuleInterop\": true,\n    \"incremental\": false,\n    \"isolatedModules\": true,\n    \"lib\": [\"es2022\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"NodeNext\",\n    \"moduleDetection\": \"force\",\n    \"moduleResolution\": \"NodeNext\",\n    \"noUncheckedIndexedAccess\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"target\": \"ES2022\"\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/nextjs.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"plugins\": [{ \"name\": \"next\" }],\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"allowJs\": true,\n    \"jsx\": \"preserve\",\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/package.json",
    "content": "{\n  \"name\": \"@internal/typescript-config\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/react-library.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/eslint.config.mjs",
    "content": "import { config } from \"@internal/eslint-config/react-internal\";\n\n/** @type {import(\"eslint\").Linter.Config} */\nexport default config;\n"
  },
  {
    "path": "packages/ui/package.json",
    "content": "{\n  \"name\": \"@internal/ui\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"exports\": {\n    \"./*\": \"./src/*.tsx\"\n  },\n  \"scripts\": {\n    \"lint\": \"eslint . --max-warnings 0\",\n    \"generate:component\": \"turbo gen react-component\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@internal/eslint-config\": \"workspace:*\",\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@types/node\": \"^22.15.3\",\n    \"@types/react\": \"19.2.3\",\n    \"@types/react-dom\": \"19.2.3\",\n    \"eslint\": \"^9.39.1\",\n    \"typescript\": \"5.9.2\"\n  },\n  \"dependencies\": {\n    \"react\": \"19.2.3\",\n    \"react-dom\": \"19.2.3\"\n  }\n}\n"
  },
  {
    "path": "packages/ui/src/button.tsx",
    "content": "\"use client\";\n\nimport { ReactNode } from \"react\";\n\ninterface ButtonProps {\n  children: ReactNode;\n  className?: string;\n  appName: string;\n}\n\nexport const Button = ({ children, className, appName }: ButtonProps) => {\n  return (\n    <button\n      className={className}\n      onClick={() => alert(`Hello from your ${appName} app!`)}\n    >\n      {children}\n    </button>\n  );\n};\n"
  },
  {
    "path": "packages/ui/src/card.tsx",
    "content": "import { type JSX } from \"react\";\n\nexport function Card({\n  className,\n  title,\n  children,\n  href,\n}: {\n  className?: string;\n  title: string;\n  children: React.ReactNode;\n  href: string;\n}): JSX.Element {\n  return (\n    <a\n      className={className}\n      href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo\"`}\n      rel=\"noopener noreferrer\"\n      target=\"_blank\"\n    >\n      <h2>\n        {title} <span>-&gt;</span>\n      </h2>\n      <p>{children}</p>\n    </a>\n  );\n}\n"
  },
  {
    "path": "packages/ui/src/code.tsx",
    "content": "import { type JSX } from \"react\";\n\nexport function Code({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}): JSX.Element {\n  return <code className={className}>{children}</code>;\n}\n"
  },
  {
    "path": "packages/ui/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/react-library.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/vue/CHANGELOG.md",
    "content": "# @json-render/vue\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Minor Changes\n\n- 9cef4e9: Dynamic forms, Vue renderer, XState Store adapter, and computed values.\n\n  ### New: `@json-render/vue` Package\n\n  Vue 3 renderer for json-render. Full feature parity with `@json-render/react` including data binding, visibility conditions, actions, validation, repeat scopes, and streaming.\n  - `defineRegistry` — create type-safe component registries from catalogs\n  - `Renderer` — render specs as Vue component trees\n  - Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`\n  - Composables: `useStateStore`, `useStateValue`, `useStateBinding`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`\n  - Streaming: `useUIStream`, `useChatUI`\n  - External store support via `StateStore` interface\n\n  ### New: `@json-render/xstate` Package\n\n  XState Store (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend.\n  - `xstateStoreStateStore({ atom })` — creates a `StateStore` from an `@xstate/store` atom\n  - Requires `@xstate/store` v3+\n\n  ### New: `$computed` Expressions\n\n  Call registered functions from prop expressions:\n  - `{ \"$computed\": \"functionName\", \"args\": { \"key\": <expression> } }` — calls a named function with resolved args\n  - Functions registered via catalog and provided at runtime through `functions` prop on `JSONUIProvider` / `createRenderer`\n  - `ComputedFunction` type exported from `@json-render/core`\n\n  ### New: `$template` Expressions\n\n  Interpolate state values into strings:\n  - `{ \"$template\": \"Hello, ${/user/name}!\" }` — replaces `${/path}` references with state values\n  - Missing paths resolve to empty string\n\n  ### New: State Watchers\n\n  React to state changes by triggering actions:\n  - `watch` field on elements maps state paths to action bindings\n  - Fires when watched values change (not on initial render)\n  - Supports cascading dependencies (e.g. country → city loading)\n  - `watch` is a top-level field on elements (sibling of type/props/children), not inside props\n  - Spec validator detects and auto-fixes `watch` placed inside props\n\n  ### New: Cross-Field Validation Functions\n\n  New built-in validation functions for cross-field comparisons:\n  - `equalTo` — alias for `matches` with clearer semantics\n  - `lessThan` — value must be less than another field (numbers, strings, coerced)\n  - `greaterThan` — value must be greater than another field\n  - `requiredIf` — required only when a condition field is truthy\n  - Validation args now resolve through `resolvePropValue` for consistent `$state` expression handling\n\n  ### New: `validateForm` Action (React)\n\n  Built-in action that validates all registered form fields at once:\n  - Runs `validateAll()` synchronously and writes `{ valid, errors }` to state\n  - Default state path: `/formValidation` (configurable via `statePath` param)\n  - Added to React schema's built-in actions list\n\n  ### Improved: shadcn/ui Validation\n\n  All form components now support validation:\n  - Checkbox, Radio, Switch — added `checks` and `validateOn` props\n  - Input, Textarea, Select — added `validateOn` prop (controls timing: change/blur/submit)\n  - Shared validation schemas reduce catalog definition duplication\n\n  ### Improved: React Provider Tree\n\n  Reordered provider nesting so `ValidationProvider` wraps `ActionProvider`, enabling `validateForm` to access validation state. Added `useOptionalValidation` hook for non-throwing access.\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n"
  },
  {
    "path": "packages/vue/README.md",
    "content": "# @json-render/vue\n\nVue 3 renderer for json-render. Turn JSON specs into Vue components with data binding, visibility, and actions.\n\n## Installation\n\n```bash\nnpm install @json-render/vue @json-render/core zod\n```\n\nPeer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.\n\n## Quick Start\n\n### 1. Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/vue/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({\n        title: z.string(),\n        description: z.string().nullable(),\n      }),\n      description: \"A card container\",\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string(),\n      }),\n      description: \"A clickable button\",\n    },\n    Input: {\n      props: z.object({\n        value: z.union([z.string(), z.record(z.unknown())]).nullable(),\n        label: z.string(),\n        placeholder: z.string().nullable(),\n      }),\n      description: \"Text input field with optional value binding\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit the form\" },\n    cancel: { description: \"Cancel and close\" },\n  },\n});\n```\n\n### 2. Define Component Implementations\n\nComponents are written using Vue's `h()` render function. `children` is a `VNode | VNode[]` — pass it directly to your container element.\n\n`defineRegistry` conditionally requires the `actions` field only when the catalog declares actions. Catalogs with `actions: {}` can omit it entirely.\n\n```typescript\nimport { h } from \"vue\";\nimport { defineRegistry } from \"@json-render/vue\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [\n        h(\"h3\", null, props.title),\n        props.description ? h(\"p\", null, props.description) : null,\n        children,\n      ]),\n    Button: ({ props, emit }) =>\n      h(\"button\", { onClick: () => emit(\"press\") }, props.label),\n    Input: ({ props, bindings }) => {\n      // Use bindings?.value with a watcher to implement two-way binding\n      return h(\"label\", null, [\n        props.label,\n        h(\"input\", {\n          placeholder: props.placeholder ?? \"\",\n          value: props.value ?? \"\",\n        }),\n      ]);\n    },\n  },\n  // actions stubs are required when the catalog declares actions:\n  actions: {\n    submit: async () => {},\n    cancel: async () => {},\n  },\n});\n```\n\n> **Tip:** Use `useBoundProp(props.value, bindings?.value)` for two-way binding, or handle the `bindings` object directly in your component.\n\n### 3. Render Specs\n\n```vue\n<script setup lang=\"ts\">\nimport { StateProvider, ActionProvider, Renderer } from \"@json-render/vue\";\nimport { registry } from \"./registry\";\n\nconst spec = { /* ... */ };\n\nfunction handleSubmit(params) {\n  console.log(\"Submit\", params);\n}\n</script>\n\n<template>\n  <StateProvider :initial-state=\"{ form: { name: '' } }\">\n    <ActionProvider :handlers=\"{ submit: handleSubmit }\">\n      <Renderer :spec=\"spec\" :registry=\"registry\" />\n    </ActionProvider>\n  </StateProvider>\n</template>\n```\n\n## Spec Format\n\nThe Vue renderer uses the same flat element map format as the React renderer:\n\n```typescript\ninterface Spec {\n  root: string;                          // Key of the root element\n  elements: Record<string, UIElement>;   // Flat map of elements by key\n  state?: Record<string, unknown>;       // Optional initial state\n}\n\ninterface UIElement {\n  type: string;                          // Component name from catalog\n  props: Record<string, unknown>;        // Component props\n  children?: string[];                   // Keys of child elements\n  visible?: VisibilityCondition;         // Visibility condition\n}\n```\n\nExample spec:\n\n```json\n{\n  \"root\": \"card-1\",\n  \"elements\": {\n    \"card-1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Welcome\" },\n      \"children\": [\"input-1\", \"btn-1\"]\n    },\n    \"input-1\": {\n      \"type\": \"Input\",\n      \"props\": {\n        \"value\": { \"$bindState\": \"/form/name\" },\n        \"label\": \"Name\",\n        \"placeholder\": \"Enter name\"\n      }\n    },\n    \"btn-1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Submit\" },\n      \"children\": []\n    }\n  }\n}\n```\n\n## Providers\n\n### StateProvider\n\nShare data across components with JSON Pointer paths:\n\n```vue\n<StateProvider :initial-state=\"{ user: { name: 'John' } }\">\n  <!-- children -->\n</StateProvider>\n```\n\n```typescript\n// In composables:\nconst { state, get, set } = useStateStore();\nconst name = get(\"/user/name\");  // \"John\"\nset(\"/user/age\", 25);\n\n// state is a ShallowRef<StateModel> — access with state.value\nconsole.log(state.value);\n```\n\n#### External Store (Controlled Mode)\n\nPass a `StateStore` to bypass the internal state and wire json-render to any state management library (Pinia, VueUse, etc.):\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/vue\";\n\n// Option 1: Use the built-in store outside of Vue\nconst store = createStateStore({ count: 0 });\n```\n\n```vue\n<StateProvider :store=\"store\">\n  <!-- children -->\n</StateProvider>\n```\n\n```typescript\n// Mutate from anywhere — Vue will re-render automatically:\nstore.set(\"/count\", 1);\n\n// Option 2: Implement the StateStore interface with your own backend\nconst piniaStore: StateStore = {\n  get: (path) => getByPath(myStore.$state, path),\n  set: (path, value) => myStore.$patch(/* ... */),\n  update: (updates) => myStore.$patch(/* ... */),\n  getSnapshot: () => myStore.$state,\n  subscribe: (listener) => myStore.$subscribe(listener),\n};\n```\n\nWhen `store` is provided, `initialState` and `onStateChange` are ignored.\n\n### ActionProvider\n\nHandle actions from components:\n\n```vue\n<ActionProvider :handlers=\"{ submit: handleSubmit, cancel: handleCancel }\">\n  <!-- children -->\n</ActionProvider>\n```\n\n### VisibilityProvider\n\nControl element visibility based on data:\n\n```vue\n<VisibilityProvider>\n  <!-- children -->\n</VisibilityProvider>\n```\n\n```json\n{\n  \"type\": \"Alert\",\n  \"props\": { \"message\": \"Error!\" },\n  \"visible\": { \"$state\": \"/form/hasError\" }\n}\n```\n\n### ValidationProvider\n\nAdd field validation:\n\n```vue\n<ValidationProvider>\n  <!-- children -->\n</ValidationProvider>\n```\n\n```typescript\n// Use validation composable:\nconst { errors, validate } = useFieldValidation(\"/form/email\", {\n  checks: [\n    { type: \"required\", message: \"Email required\" },\n    { type: \"email\", message: \"Invalid email\" },\n  ],\n});\n```\n\n## Composables\n\n| Composable | Purpose |\n|------------|---------|\n| `useStateStore()` | Access state context (`state` as `ShallowRef`, `get`, `set`, `update`) |\n| `useStateValue(path)` | Get single value from state |\n| `useStateBinding(path)` | Two-way data binding (deprecated — use `$bindState` instead) |\n| `useIsVisible(condition)` | Check if a visibility condition is met |\n| `useActions()` | Access action context |\n| `useAction(binding)` | Get a single action dispatch function |\n| `useFieldValidation(path, config)` | Field validation state |\n\n> **Note:** `useStateStore().state` returns a `ShallowRef<StateModel>` — use `state.value` to access the underlying object. This differs from the React renderer where `state` is a plain object.\n\n## Visibility Conditions\n\n```typescript\n// Truthiness check\n{ \"$state\": \"/user/isAdmin\" }\n\n// Auth state (use state path)\n{ \"$state\": \"/auth/isSignedIn\" }\n\n// Comparisons (flat style)\n{ \"$state\": \"/status\", \"eq\": \"active\" }\n{ \"$state\": \"/count\", \"gt\": 10 }\n\n// Negation\n{ \"$state\": \"/maintenance\", \"not\": true }\n\n// Multiple conditions (implicit AND)\n[\n  { \"$state\": \"/feature/enabled\" },\n  { \"$state\": \"/maintenance\", \"not\": true }\n]\n\n// Always / never\ntrue   // always visible\nfalse  // never visible\n```\n\nTypeScript helpers from `@json-render/core`:\n\n```typescript\nimport { visibility } from \"@json-render/core\";\n\nvisibility.when(\"/path\")       // { $state: \"/path\" }\nvisibility.unless(\"/path\")     // { $state: \"/path\", not: true }\nvisibility.eq(\"/path\", val)    // { $state: \"/path\", eq: val }\nvisibility.neq(\"/path\", val)   // { $state: \"/path\", neq: val }\nvisibility.and(cond1, cond2)  // { $and: [cond1, cond2] }\nvisibility.always             // true\nvisibility.never              // false\n```\n\n## Dynamic Prop Expressions\n\nAny prop value can use data-driven expressions that resolve at render time. The renderer resolves these transparently before passing props to components.\n\n```json\n{\n  \"type\": \"Badge\",\n  \"props\": {\n    \"label\": { \"$state\": \"/user/role\" },\n    \"color\": {\n      \"$cond\": { \"$state\": \"/user/role\", \"eq\": \"admin\" },\n      \"$then\": \"red\",\n      \"$else\": \"gray\"\n    }\n  }\n}\n```\n\nFor two-way binding, use `{ \"$bindState\": \"/path\" }` on the natural value prop. Inside repeat scopes, use `{ \"$bindItem\": \"field\" }` instead. Components receive resolved `bindings` with the state path for each bound prop.\n\nSee [@json-render/core](../core/README.md) for full expression syntax.\n\n## Built-in Actions\n\nThe `setState`, `pushState`, `removeState`, and `validateForm` actions are built into the Vue schema and handled automatically by `ActionProvider`. They are injected into AI prompts without needing to be declared in your catalog's `actions`. They update the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Switch Tab\" },\n  \"on\": {\n    \"press\": {\n      \"action\": \"setState\",\n      \"params\": { \"statePath\": \"/activeTab\", \"value\": \"settings\" }\n    }\n  },\n  \"children\": []\n}\n```\n\n## Component Props\n\nWhen using `defineRegistry`, components receive these props via their render function:\n\n```typescript\nimport type { VNode } from \"vue\";\n\ninterface ComponentContext<P> {\n  props: P;                          // Typed props from the catalog (expressions resolved)\n  children?: VNode | VNode[];        // Rendered children (for container components)\n  emit: (event: string) => void;     // Emit a named event (always defined)\n  on: (event: string) => EventHandle; // Get event handle with metadata\n  loading?: boolean;                 // Whether the parent is loading\n  bindings?: Record<string, string>; // State paths for $bindState/$bindItem expressions\n}\n\ninterface EventHandle {\n  emit: () => void;            // Fire the event\n  shouldPreventDefault: boolean; // Whether any binding requested preventDefault\n  bound: boolean;              // Whether any handler is bound\n}\n```\n\nUse `emit(\"press\")` for simple event firing. Use `on(\"click\")` when you need to check metadata like `shouldPreventDefault` or `bound`:\n\n```typescript\nLink: ({ props, on }) => {\n  const click = on(\"click\");\n  return h(\"a\", {\n    href: props.href,\n    onClick: (e: MouseEvent) => {\n      if (click.shouldPreventDefault) e.preventDefault();\n      click.emit();\n    },\n  }, props.label);\n},\n```\n\n### `BaseComponentProps`\n\nFor building reusable component libraries that are not tied to a specific catalog, use the catalog-agnostic `BaseComponentProps` type:\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/vue\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>\n  h(\"div\", null, [props.title, children]);\n```\n\n## Generate AI Prompts\n\n```typescript\nconst systemPrompt = catalog.prompt();\n// Returns detailed prompt with component/action descriptions\n```\n\n## Full Example\n\n```typescript\nimport { h } from \"vue\";\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/vue/schema\";\nimport { defineRegistry, Renderer, StateProvider } from \"@json-render/vue\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Greeting: {\n      props: z.object({ name: z.string() }),\n      description: \"Displays a greeting\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Greeting: ({ props }) => h(\"h1\", null, `Hello, ${props.name}!`),\n  },\n});\n\nconst spec = {\n  root: \"greeting-1\",\n  elements: {\n    \"greeting-1\": {\n      type: \"Greeting\",\n      props: { name: \"World\" },\n      children: [],\n    },\n  },\n};\n\n// In your App.vue:\n// <StateProvider>\n//   <Renderer :spec=\"spec\" :registry=\"registry\" />\n// </StateProvider>\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| `schema` | Element tree schema (includes built-in actions: `setState`, `pushState`, `removeState`) |\n| `useStateStore` | Access state context (`state` is `ShallowRef<StateModel>`) |\n| `useStateValue` | Get single value from state |\n| `useActions` | Access actions context |\n| `useAction` | Get a single action dispatch function |\n| `createStateStore` | Create a framework-agnostic in-memory `StateStore` |\n\n### Types\n\n| Export | Purpose |\n|--------|---------|\n| `ComponentContext` | Typed component render function context (catalog-aware) |\n| `BaseComponentProps` | Catalog-agnostic base type for reusable component libraries |\n| `EventHandle` | Event handle with `emit()`, `shouldPreventDefault`, `bound` |\n| `ActionProviderProps` | Props for `ActionProvider` |\n| `ValidationProviderProps` | Props for `ValidationProvider` |\n| `ComponentFn` | Component render function type |\n| `SetState` | State setter type |\n| `StateModel` | State model type |\n| `StateStore` | Interface for plugging in external state management |\n\n## Differences from `@json-render/react`\n\n| API | React | Vue | Note |\n|-----|-------|-----|------|\n| `useStateStore().state` | `StateModel` | `ShallowRef<StateModel>` | Vue reactivity; use `state.value` |\n| `children` type | `React.ReactNode` | `VNode \\| VNode[]` | Platform-specific |\n| `useBoundProp` | exported | exported | Same API; returns `[value, setValue]` |\n| Streaming hooks | `useUIStream`, `useChatUI` | `useUIStream`, `useChatUI` | Same API; returns Vue `Ref` values |\n"
  },
  {
    "path": "packages/vue/package.json",
    "content": "{\n  \"name\": \"@json-render/vue\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Vue renderer for @json-render/core. JSON becomes Vue components.\",\n  \"keywords\": [\n    \"json\",\n    \"ui\",\n    \"vue\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"renderer\",\n    \"streaming\",\n    \"components\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/vue\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./schema\": {\n      \"types\": \"./dist/schema.d.ts\",\n      \"import\": \"./dist/schema.mjs\",\n      \"require\": \"./dist/schema.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"@vue/test-utils\": \"^2.4.6\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"vue\": \"^3.5.0\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"^3.5.0\",\n    \"zod\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/vue/src/catalog-types.ts",
    "content": "import type { VNode } from \"vue\";\nimport type {\n  Catalog,\n  InferCatalogComponents,\n  InferCatalogActions,\n  InferComponentProps,\n  InferActionParams,\n  StateModel,\n} from \"@json-render/core\";\n\nexport type { StateModel };\n\n// =============================================================================\n// State Types\n// =============================================================================\n\n/**\n * State setter function for updating application state\n */\nexport type SetState = (\n  updater: (prev: Record<string, unknown>) => Record<string, unknown>,\n) => void;\n\n// =============================================================================\n// Component Types\n// =============================================================================\n\n/**\n * Handle returned by the `on()` function for a specific event.\n * Provides metadata about the event binding and a method to fire it.\n *\n * @example\n * ```ts\n * const press = on(\"press\");\n * if (press.shouldPreventDefault) e.preventDefault();\n * press.emit();\n * ```\n */\nexport interface EventHandle {\n  /** Fire the event (resolve action bindings) */\n  emit: () => void;\n  /** Whether any binding requested preventDefault */\n  shouldPreventDefault: boolean;\n  /** Whether any handler is bound to this event */\n  bound: boolean;\n}\n\n/**\n * Catalog-agnostic base type for component render function arguments.\n * Use this when building reusable component libraries that are not tied to a specific catalog.\n *\n * @example\n * ```ts\n * const Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>\n *   h('div', null, [props.title, children])\n * ```\n */\nexport interface BaseComponentProps<P = Record<string, unknown>> {\n  props: P;\n  /** Rendered children (from the default slot) */\n  children?: VNode | VNode[];\n  /** Simple event emitter (shorthand). Fires the event and returns void. */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata. Use when you need shouldPreventDefault or bound checks. */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n\n/**\n * Context passed to component render functions\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = (ctx) => {\n *   return h('button', { onClick: () => ctx.emit(\"press\") }, ctx.props.label)\n * }\n */\nexport interface ComponentContext<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> extends BaseComponentProps<InferComponentProps<C, K>> {}\n\n/**\n * Component render function type for Vue\n * @example\n * const Button: ComponentFn<typeof catalog, 'Button'> = ({ props, emit }) =>\n *   h('button', { onClick: () => emit(\"press\") }, props.label)\n */\nexport type ComponentFn<\n  C extends Catalog,\n  K extends keyof InferCatalogComponents<C>,\n> = (ctx: ComponentContext<C, K>) => VNode | VNode[] | null | string;\n\n/**\n * Registry of all component render functions for a catalog\n * @example\n * const components: Components<typeof myCatalog> = {\n *   Button: ({ props }) => h('button', null, props.label),\n *   Input: ({ props }) => h('input', { placeholder: props.placeholder }),\n * };\n */\nexport type Components<C extends Catalog> = {\n  [K in keyof InferCatalogComponents<C>]: ComponentFn<C, K>;\n};\n\n// =============================================================================\n// Action Types\n// =============================================================================\n\n/**\n * Action handler function type\n * @example\n * const viewCustomers: ActionFn<typeof catalog, 'viewCustomers'> = async (params, setState) => {\n *   const data = await fetch('/api/customers');\n *   setState(prev => ({ ...prev, customers: data }));\n * };\n */\nexport type ActionFn<\n  C extends Catalog,\n  K extends keyof InferCatalogActions<C>,\n> = (\n  params: InferActionParams<C, K> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Registry of all action handlers for a catalog\n * @example\n * const actions: Actions<typeof myCatalog> = {\n *   viewCustomers: async (params, setState) => { ... },\n *   createCustomer: async (params, setState) => { ... },\n * };\n */\nexport type Actions<C extends Catalog> = {\n  [K in keyof InferCatalogActions<C>]: ActionFn<C, K>;\n};\n\n/**\n * True when the catalog declares at least one action, false otherwise.\n * Used by defineRegistry to conditionally require the `actions` field.\n */\nexport type CatalogHasActions<C extends Catalog> = [\n  InferCatalogActions<C>,\n] extends [never]\n  ? false\n  : [keyof InferCatalogActions<C>] extends [never]\n    ? false\n    : true;\n"
  },
  {
    "path": "packages/vue/src/composables/actions.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport { StateProvider, useStateStore } from \"./state\";\nimport { ActionProvider, useActions, useAction } from \"./actions\";\n\n/** Mount StateProvider → ActionProvider with a child that captures context. */\nfunction withProviders<T>(\n  composable: () => T,\n  handlers: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<void>\n  > = {},\n  initialState: Record<string, unknown> = {},\n): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: { initialState } as any,\n    slots: {\n      default: () =>\n        h(ActionProvider as Component, { handlers } as any, {\n          default: () => h(Child),\n        }),\n    },\n  });\n  return { result };\n}\n\ndescribe(\"ActionProvider — provide/inject\", () => {\n  it(\"useActions() throws outside a provider\", () => {\n    const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    expect(() => useActions()).toThrow(\n      \"useActions must be used within an ActionProvider\",\n    );\n    warn.mockRestore();\n  });\n});\n\ndescribe(\"ActionProvider — custom handler dispatch\", () => {\n  it(\"calling execute invokes the registered handler\", async () => {\n    const handler = vi.fn().mockResolvedValue(undefined);\n    const { result } = withProviders(() => useActions(), { myAction: handler });\n    await result.execute({ action: \"myAction\", params: { x: 1 } });\n    expect(handler).toHaveBeenCalledOnce();\n  });\n\n  it(\"handler receives the resolved params\", async () => {\n    const handler = vi.fn().mockResolvedValue(undefined);\n    const { result } = withProviders(() => useActions(), { myAction: handler });\n    await result.execute({ action: \"myAction\", params: { x: 1, y: \"hello\" } });\n    expect(handler).toHaveBeenCalledWith({ x: 1, y: \"hello\" });\n  });\n\n  it(\"console.warn is called for unknown actions\", async () => {\n    const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const { result } = withProviders(() => useActions());\n    await result.execute({ action: \"unknownAction\" });\n    expect(warn).toHaveBeenCalledWith(expect.stringContaining(\"unknownAction\"));\n    warn.mockRestore();\n  });\n});\n\ndescribe(\"ActionProvider — built-in setState integration\", () => {\n  it(\"executing setState updates state via the provider chain\", async () => {\n    let stateCtx!: ReturnType<typeof useStateStore>;\n    let actionsCtx!: ReturnType<typeof useActions>;\n\n    const Child = defineComponent({\n      setup() {\n        stateCtx = useStateStore();\n        actionsCtx = useActions();\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { v: 0 } } as any,\n      slots: {\n        default: () =>\n          h(ActionProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    await actionsCtx.execute({\n      action: \"setState\",\n      params: { statePath: \"/v\", value: 42 },\n    });\n    expect(stateCtx.state.value).toEqual({ v: 42 });\n  });\n});\n\ndescribe(\"useAction\", () => {\n  it(\"returns { execute, isLoading: false } before execution\", () => {\n    const { result } = withProviders(() => useAction({ action: \"myAction\" }));\n    expect(typeof result.execute).toBe(\"function\");\n    expect(result.isLoading.value).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/composables/actions.ts",
    "content": "import {\n  computed,\n  defineComponent,\n  h,\n  inject,\n  provide,\n  ref,\n  watch,\n  type ComputedRef,\n  type PropType,\n} from \"vue\";\nimport {\n  resolveAction,\n  executeAction,\n  type ActionBinding,\n  type ActionHandler,\n  type ActionConfirm,\n  type ResolvedAction,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\nimport { useOptionalValidation } from \"./validation\";\n\n/**\n * Generate a unique ID for use with the \"$id\" token.\n */\nlet idCounter = 0;\nfunction generateUniqueId(): string {\n  idCounter += 1;\n  return `${Date.now()}-${idCounter}`;\n}\n\n/**\n * Deep-resolve dynamic value references within an object.\n *\n * Supported tokens:\n * - `{ $state: \"/statePath\" }` - read a value from state\n * - `\"$id\"` (string) or `{ \"$id\": true }` - generate a unique ID\n */\nfunction deepResolveValue(\n  value: unknown,\n  get: (path: string) => unknown,\n): unknown {\n  if (value === null || value === undefined) return value;\n\n  if (value === \"$id\") {\n    return generateUniqueId();\n  }\n\n  if (typeof value === \"object\" && !Array.isArray(value)) {\n    const obj = value as Record<string, unknown>;\n    const keys = Object.keys(obj);\n\n    if (keys.length === 1 && typeof obj.$state === \"string\") {\n      return get(obj.$state as string);\n    }\n\n    if (keys.length === 1 && \"$id\" in obj) {\n      return generateUniqueId();\n    }\n  }\n\n  if (Array.isArray(value)) {\n    return value.map((item) => deepResolveValue(item, get));\n  }\n\n  if (typeof value === \"object\") {\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n      resolved[key] = deepResolveValue(val, get);\n    }\n    return resolved;\n  }\n\n  return value;\n}\n\n/**\n * Pending confirmation state\n */\nexport interface PendingConfirmation {\n  action: ResolvedAction;\n  handler: ActionHandler;\n  resolve: () => void;\n  reject: () => void;\n}\n\n/**\n * Action context value\n */\nexport interface ActionContextValue {\n  handlers: Record<string, ActionHandler>;\n  loadingActions: Set<string>;\n  pendingConfirmation: PendingConfirmation | null;\n  execute: (binding: ActionBinding) => Promise<void>;\n  confirm: () => void;\n  cancel: () => void;\n  registerHandler: (name: string, handler: ActionHandler) => void;\n}\n\nconst ACTIONS_KEY = Symbol(\"json-render:actions\");\n\nexport interface ActionProviderProps {\n  handlers?: Record<string, ActionHandler>;\n  navigate?: (path: string) => void;\n}\n\n/**\n * Provider for action execution\n */\nexport const ActionProvider = defineComponent({\n  name: \"ActionProvider\",\n  props: {\n    handlers: {\n      type: Object as PropType<Record<string, ActionHandler>>,\n      default: () => ({}),\n    },\n    navigate: {\n      type: Function as PropType<(path: string) => void>,\n      default: undefined,\n    },\n  },\n  setup(props, { slots }) {\n    const { get, set, getSnapshot } = useStateStore();\n    const validation = useOptionalValidation();\n\n    const handlers = ref<Record<string, ActionHandler>>(props.handlers ?? {});\n    const loadingActions = ref<Set<string>>(new Set());\n    const pendingConfirmation = ref<PendingConfirmation | null>(null);\n\n    // Sync handlers when prop changes\n    watch(\n      () => props.handlers,\n      (newHandlers) => {\n        if (newHandlers) handlers.value = newHandlers;\n      },\n    );\n\n    const registerHandler = (name: string, handler: ActionHandler) => {\n      handlers.value = { ...handlers.value, [name]: handler };\n    };\n\n    const execute = async (binding: ActionBinding): Promise<void> => {\n      const resolved = resolveAction(binding, getSnapshot());\n\n      // Built-in: setState\n      if (resolved.action === \"setState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const value = resolved.params.value;\n        if (statePath) {\n          set(statePath, value);\n        }\n        return;\n      }\n\n      // Built-in: pushState\n      if (resolved.action === \"pushState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const rawValue = resolved.params.value;\n        if (statePath) {\n          const resolvedValue = deepResolveValue(rawValue, get);\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(statePath, [...arr, resolvedValue]);\n          const clearStatePath = resolved.params.clearStatePath as\n            | string\n            | undefined;\n          if (clearStatePath) {\n            set(clearStatePath, \"\");\n          }\n        }\n        return;\n      }\n\n      // Built-in: removeState\n      if (resolved.action === \"removeState\" && resolved.params) {\n        const statePath = resolved.params.statePath as string;\n        const index = resolved.params.index as number;\n        if (statePath !== undefined && index !== undefined) {\n          const arr = (get(statePath) as unknown[] | undefined) ?? [];\n          set(\n            statePath,\n            arr.filter((_, i) => i !== index),\n          );\n        }\n        return;\n      }\n\n      // Built-in: validateForm — triggers validateAll and writes result to state\n      if (resolved.action === \"validateForm\") {\n        const validateAll = validation?.validateAll;\n        if (!validateAll) {\n          console.warn(\n            \"validateForm action was dispatched but no ValidationProvider is connected. \" +\n              \"Ensure ValidationProvider is rendered inside the provider tree.\",\n          );\n          return;\n        }\n        const valid = validateAll();\n        const errors: Record<string, string[]> = {};\n        for (const [path, fs] of Object.entries(validation.fieldStates)) {\n          if (fs.result && !fs.result.valid) {\n            errors[path] = fs.result.errors;\n          }\n        }\n        const statePath =\n          (resolved.params?.statePath as string) || \"/formValidation\";\n        set(statePath, { valid, errors });\n        return;\n      }\n\n      // Built-in: push (navigation)\n      if (resolved.action === \"push\" && resolved.params) {\n        const screen = resolved.params.screen as string;\n        if (screen) {\n          const currentScreen = get(\"/currentScreen\") as string | undefined;\n          const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n          if (currentScreen) {\n            set(\"/navStack\", [...navStack, currentScreen]);\n          } else {\n            set(\"/navStack\", [...navStack, \"\"]);\n          }\n          set(\"/currentScreen\", screen);\n        }\n        return;\n      }\n\n      // Built-in: pop (navigation)\n      if (resolved.action === \"pop\") {\n        const navStack = (get(\"/navStack\") as string[] | undefined) ?? [];\n        if (navStack.length > 0) {\n          const previousScreen = navStack[navStack.length - 1];\n          set(\"/navStack\", navStack.slice(0, -1));\n          if (previousScreen) {\n            set(\"/currentScreen\", previousScreen);\n          } else {\n            set(\"/currentScreen\", undefined);\n          }\n        }\n        return;\n      }\n\n      const handler = handlers.value[resolved.action];\n\n      if (!handler) {\n        console.warn(`No handler registered for action: ${resolved.action}`);\n        return;\n      }\n\n      // If confirmation is required, show dialog first\n      if (resolved.confirm) {\n        await new Promise<void>((resolve, reject) => {\n          pendingConfirmation.value = {\n            action: resolved,\n            handler,\n            resolve: () => {\n              pendingConfirmation.value = null;\n              resolve();\n            },\n            reject: () => {\n              pendingConfirmation.value = null;\n              reject(new Error(\"Action cancelled\"));\n            },\n          };\n        });\n\n        const addLoading = new Set(loadingActions.value);\n        addLoading.add(resolved.action);\n        loadingActions.value = addLoading;\n        try {\n          await executeAction({\n            action: resolved,\n            handler,\n            setState: set,\n            navigate: props.navigate,\n            executeAction: async (name) => {\n              const subBinding: ActionBinding = { action: name };\n              await execute(subBinding);\n            },\n          });\n        } finally {\n          const removeLoading = new Set(loadingActions.value);\n          removeLoading.delete(resolved.action);\n          loadingActions.value = removeLoading;\n        }\n        return;\n      }\n\n      // Execute immediately\n      const addLoading = new Set(loadingActions.value);\n      addLoading.add(resolved.action);\n      loadingActions.value = addLoading;\n      try {\n        await executeAction({\n          action: resolved,\n          handler,\n          setState: set,\n          navigate: props.navigate,\n          executeAction: async (name) => {\n            const subBinding: ActionBinding = { action: name };\n            await execute(subBinding);\n          },\n        });\n      } finally {\n        const removeLoading = new Set(loadingActions.value);\n        removeLoading.delete(resolved.action);\n        loadingActions.value = removeLoading;\n      }\n    };\n\n    const confirm = () => pendingConfirmation.value?.resolve();\n    const cancel = () => pendingConfirmation.value?.reject();\n\n    provide<ActionContextValue>(ACTIONS_KEY, {\n      get handlers() {\n        return handlers.value;\n      },\n      get loadingActions() {\n        return loadingActions.value;\n      },\n      get pendingConfirmation() {\n        return pendingConfirmation.value;\n      },\n      execute,\n      confirm,\n      cancel,\n      registerHandler,\n    });\n\n    return () => slots.default?.();\n  },\n});\n\n/**\n * Composable to access action context\n */\nexport function useActions(): ActionContextValue {\n  const ctx = inject<ActionContextValue>(ACTIONS_KEY);\n  if (!ctx) {\n    throw new Error(\"useActions must be used within an ActionProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Composable to execute an action binding\n */\nexport function useAction(binding: ActionBinding): {\n  execute: () => Promise<void>;\n  isLoading: ComputedRef<boolean>;\n} {\n  const ctx = useActions();\n  return {\n    execute: () => ctx.execute(binding),\n    isLoading: computed(() => ctx.loadingActions.has(binding.action)),\n  };\n}\n\n// =============================================================================\n// ConfirmDialog component\n// =============================================================================\n\n/**\n * Props for ConfirmDialog component\n */\nexport interface ConfirmDialogProps {\n  confirm: ActionConfirm;\n  onConfirm: () => void;\n  onCancel: () => void;\n}\n\n/**\n * Default confirmation dialog component\n */\nexport const ConfirmDialog = defineComponent({\n  name: \"ConfirmDialog\",\n  props: {\n    confirm: {\n      type: Object as PropType<ActionConfirm>,\n      required: true,\n    },\n    onConfirm: {\n      type: Function as PropType<() => void>,\n      required: true,\n    },\n    onCancel: {\n      type: Function as PropType<() => void>,\n      required: true,\n    },\n  },\n  setup(props) {\n    return () => {\n      const isDanger = props.confirm.variant === \"danger\";\n\n      return h(\n        \"div\",\n        {\n          style: {\n            position: \"fixed\",\n            inset: \"0\",\n            backgroundColor: \"rgba(0, 0, 0, 0.5)\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            zIndex: \"50\",\n          },\n          onClick: props.onCancel,\n        },\n        [\n          h(\n            \"div\",\n            {\n              style: {\n                backgroundColor: \"white\",\n                borderRadius: \"8px\",\n                padding: \"24px\",\n                maxWidth: \"400px\",\n                width: \"100%\",\n                boxShadow: \"0 20px 25px -5px rgba(0, 0, 0, 0.1)\",\n              },\n              onClick: (e: MouseEvent) => e.stopPropagation(),\n            },\n            [\n              h(\n                \"h3\",\n                {\n                  style: {\n                    margin: \"0 0 8px 0\",\n                    fontSize: \"18px\",\n                    fontWeight: \"600\",\n                  },\n                },\n                props.confirm.title,\n              ),\n              h(\n                \"p\",\n                {\n                  style: { margin: \"0 0 24px 0\", color: \"#6b7280\" },\n                },\n                props.confirm.message,\n              ),\n              h(\n                \"div\",\n                {\n                  style: {\n                    display: \"flex\",\n                    gap: \"12px\",\n                    justifyContent: \"flex-end\",\n                  },\n                },\n                [\n                  h(\n                    \"button\",\n                    {\n                      style: {\n                        padding: \"8px 16px\",\n                        borderRadius: \"6px\",\n                        border: \"1px solid #d1d5db\",\n                        backgroundColor: \"white\",\n                        cursor: \"pointer\",\n                      },\n                      onClick: props.onCancel,\n                    },\n                    props.confirm.cancelLabel ?? \"Cancel\",\n                  ),\n                  h(\n                    \"button\",\n                    {\n                      style: {\n                        padding: \"8px 16px\",\n                        borderRadius: \"6px\",\n                        border: \"none\",\n                        backgroundColor: isDanger ? \"#dc2626\" : \"#3b82f6\",\n                        color: \"white\",\n                        cursor: \"pointer\",\n                      },\n                      onClick: props.onConfirm,\n                    },\n                    props.confirm.confirmLabel ?? \"Confirm\",\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ],\n      );\n    };\n  },\n});\n"
  },
  {
    "path": "packages/vue/src/composables/repeat-scope.ts",
    "content": "import { defineComponent, inject, provide, type PropType } from \"vue\";\n\n/**\n * Repeat scope value provided to child elements inside a repeated element.\n */\nexport interface RepeatScopeValue {\n  /** The current array item object */\n  item: unknown;\n  /** Index of the current item in the array */\n  index: number;\n  /** Absolute state path to the current array item (e.g. \"/todos/0\") — used for statePath two-way binding */\n  basePath: string;\n}\n\nconst REPEAT_SCOPE_KEY = Symbol(\"json-render:repeat-scope\");\n\n/**\n * Provides repeat scope to child elements so $item and $index expressions resolve correctly.\n */\nexport const RepeatScopeProvider = defineComponent({\n  name: \"RepeatScopeProvider\",\n  props: {\n    item: {\n      required: true,\n    },\n    index: {\n      type: Number,\n      required: true,\n    },\n    basePath: {\n      type: String as PropType<string>,\n      required: true,\n    },\n  },\n  setup(props, { slots }) {\n    provide(REPEAT_SCOPE_KEY, props as RepeatScopeValue);\n    return () => slots.default?.();\n  },\n});\n\n/**\n * Read the current repeat scope (or null if not inside a repeated element).\n */\nexport function useRepeatScope(): RepeatScopeValue | null {\n  return (\n    inject<RepeatScopeValue>(\n      REPEAT_SCOPE_KEY,\n      null as unknown as RepeatScopeValue,\n    ) ?? null\n  );\n}\n"
  },
  {
    "path": "packages/vue/src/composables/state.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport { createStateStore } from \"@json-render/core\";\nimport { StateProvider, useStateStore } from \"./state\";\n\n/** Mount a StateProvider with a child that captures the injected context. */\nfunction withProvider<T>(\n  composable: () => T,\n  props: Record<string, unknown> = {},\n): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: props as any,\n    slots: { default: () => h(Child) },\n  });\n  return { result };\n}\n\ndescribe(\"StateProvider — provide/inject\", () => {\n  it(\"useStateStore() throws outside a provider\", () => {\n    // inject() returns undefined outside of component setup; our guard throws\n    const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    expect(() => useStateStore()).toThrow(\n      \"useStateStore must be used within a StateProvider\",\n    );\n    warn.mockRestore();\n  });\n\n  it(\"child receives the context from StateProvider\", () => {\n    const { result } = withProvider(() => useStateStore());\n    expect(result).toBeDefined();\n    expect(result.state).toBeDefined();\n    expect(typeof result.get).toBe(\"function\");\n    expect(typeof result.set).toBe(\"function\");\n    expect(typeof result.update).toBe(\"function\");\n  });\n\n  it(\"state.value is a plain object (not a wrapper type)\", () => {\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: { x: 1 },\n    });\n    expect(result.state.value).toEqual({ x: 1 });\n    // Should be a plain object, not a Vue Proxy of a ShallowRef wrapper\n    expect(typeof result.state.value).toBe(\"object\");\n  });\n});\n\ndescribe(\"StateProvider (uncontrolled) — reactivity\", () => {\n  it(\"state.value reflects initialState on mount\", () => {\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: { count: 5 },\n    });\n    expect(result.state.value).toEqual({ count: 5 });\n  });\n\n  it(\"after set(), state.value is updated synchronously\", () => {\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: { x: 0 },\n    });\n    result.set(\"/x\", 42);\n    expect(result.state.value).toEqual({ x: 42 });\n  });\n\n  it(\"after update(), all paths are reflected in state.value\", () => {\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: {},\n    });\n    result.update({ \"/a\": 1, \"/b\": \"hello\" });\n    expect(result.state.value).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"onStateChange is fired with the changes array on set\", () => {\n    const onChange = vi.fn();\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: {},\n      onStateChange: onChange,\n    });\n    result.set(\"/name\", \"Alice\");\n    expect(onChange).toHaveBeenCalledOnce();\n    expect(onChange).toHaveBeenCalledWith([{ path: \"/name\", value: \"Alice\" }]);\n  });\n\n  it(\"onStateChange is fired once with all changed entries on update\", () => {\n    const onChange = vi.fn();\n    const { result } = withProvider(() => useStateStore(), {\n      initialState: {},\n      onStateChange: onChange,\n    });\n    result.update({ \"/a\": 1, \"/b\": 2 });\n    expect(onChange).toHaveBeenCalledOnce();\n    const [changes] = onChange.mock.calls[0]!;\n    expect(changes).toEqual(\n      expect.arrayContaining([\n        { path: \"/a\", value: 1 },\n        { path: \"/b\", value: 2 },\n      ]),\n    );\n  });\n});\n\ndescribe(\"StateProvider (controlled mode)\", () => {\n  it(\"state.value reads from the external store snapshot\", () => {\n    const store = createStateStore({ x: 10 });\n    const { result } = withProvider(() => useStateStore(), { store });\n    expect(result.state.value).toEqual({ x: 10 });\n  });\n\n  it(\"set() writes through to the external store\", () => {\n    const store = createStateStore({ x: 0 });\n    const { result } = withProvider(() => useStateStore(), { store });\n    result.set(\"/x\", 99);\n    expect(store.getSnapshot()).toEqual({ x: 99 });\n  });\n\n  it(\"external store mutation triggers state.value update\", () => {\n    const store = createStateStore({ x: 0 });\n    const { result } = withProvider(() => useStateStore(), { store });\n    store.set(\"/x\", 99);\n    expect(result.state.value).toEqual({ x: 99 });\n  });\n\n  it(\"onStateChange is NOT called in controlled mode\", () => {\n    const store = createStateStore({ x: 0 });\n    const onChange = vi.fn();\n    const { result } = withProvider(() => useStateStore(), {\n      store,\n      onStateChange: onChange,\n    });\n    result.set(\"/x\", 99);\n    expect(onChange).not.toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/composables/state.ts",
    "content": "import {\n  computed,\n  defineComponent,\n  inject,\n  onUnmounted,\n  provide,\n  ref,\n  shallowRef,\n  watch,\n  type ComputedRef,\n  type PropType,\n  type ShallowRef,\n} from \"vue\";\nimport {\n  createStateStore,\n  getByPath,\n  type StateModel,\n  type StateStore,\n} from \"@json-render/core\";\nimport { flattenToPointers } from \"@json-render/core/store-utils\";\n\n/**\n * State context value\n */\nexport interface StateContextValue {\n  /** Reactive state snapshot — use state.value in reactive contexts */\n  state: ShallowRef<StateModel>;\n  /** Get a value by path */\n  get: (path: string) => unknown;\n  /** Set a value by path */\n  set: (path: string, value: unknown) => void;\n  /** Update multiple values at once */\n  update: (updates: Record<string, unknown>) => void;\n  /** Return the live state snapshot from the underlying store (not through Vue reactivity). */\n  getSnapshot: () => StateModel;\n}\n\nconst STATE_KEY = Symbol(\"json-render:state\");\n\n/**\n * Props for StateProvider\n */\nexport interface StateProviderProps {\n  /**\n   * External store that owns the state. When provided, the provider operates\n   * in **controlled mode** — `initialState` and `onStateChange` are ignored\n   * and the store is the single source of truth.\n   */\n  store?: StateStore;\n  /** Initial state model (used only in uncontrolled mode) */\n  initialState?: StateModel;\n  /**\n   * Callback when state changes (used only in uncontrolled mode).\n   * Called once per `set` or `update` with all changed entries.\n   */\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n}\n\n/**\n * Provider for state model context.\n *\n * Supports two modes:\n * - **Controlled**: pass a `store` prop (e.g. backed by Redux / Zustand).\n * - **Uncontrolled** (default): omit `store` and optionally pass\n *   `initialState` / `onStateChange`.\n */\nexport const StateProvider = defineComponent({\n  name: \"StateProvider\",\n  props: {\n    store: {\n      type: Object as PropType<StateStore>,\n      default: undefined,\n    },\n    initialState: {\n      type: Object as PropType<StateModel>,\n      default: undefined,\n    },\n    onStateChange: {\n      type: Function as PropType<\n        (changes: Array<{ path: string; value: unknown }>) => void\n      >,\n      default: undefined,\n    },\n  },\n  setup(props, { slots }) {\n    const isControlled = !!props.store;\n\n    // Use external store (controlled) or create internal store (uncontrolled)\n    const internalStore = !isControlled\n      ? createStateStore(props.initialState ?? {})\n      : null;\n    const store: StateStore = props.store ?? internalStore!;\n\n    const state = shallowRef<StateModel>(store.getSnapshot());\n\n    const unsubscribe = store.subscribe(() => {\n      state.value = store.getSnapshot();\n    });\n    onUnmounted(unsubscribe);\n\n    // Sync external initialState changes (uncontrolled mode only)\n    if (!isControlled) {\n      let prevFlat: Record<string, unknown> =\n        props.initialState && Object.keys(props.initialState).length > 0\n          ? flattenToPointers(props.initialState)\n          : {};\n\n      watch(\n        () => props.initialState,\n        (newInitialState) => {\n          if (!newInitialState) return;\n          const nextFlat =\n            Object.keys(newInitialState).length > 0\n              ? flattenToPointers(newInitialState)\n              : {};\n          const allKeys = new Set([\n            ...Object.keys(prevFlat),\n            ...Object.keys(nextFlat),\n          ]);\n          const updates: Record<string, unknown> = {};\n          for (const key of allKeys) {\n            if (prevFlat[key] !== nextFlat[key]) {\n              updates[key] = key in nextFlat ? nextFlat[key] : undefined;\n            }\n          }\n          prevFlat = nextFlat;\n          if (Object.keys(updates).length > 0) {\n            store.update(updates);\n          }\n        },\n      );\n    }\n\n    // Keep onStateChange in a ref so it always reads the latest callback\n    const onStateChangeRef = ref(props.onStateChange);\n    watch(\n      () => props.onStateChange,\n      (fn) => {\n        onStateChangeRef.value = fn;\n      },\n    );\n\n    const get = (path: string) => store.get(path);\n    const getSnapshot = () => store.getSnapshot();\n\n    const set = (path: string, value: unknown) => {\n      const prev = store.getSnapshot();\n      store.set(path, value);\n      if (!isControlled && store.getSnapshot() !== prev) {\n        onStateChangeRef.value?.([{ path, value }]);\n      }\n    };\n\n    const update = (updates: Record<string, unknown>) => {\n      const prev = store.getSnapshot();\n      store.update(updates);\n      if (!isControlled && store.getSnapshot() !== prev) {\n        const changes: Array<{ path: string; value: unknown }> = [];\n        for (const [path, value] of Object.entries(updates)) {\n          if (getByPath(prev, path) !== value) {\n            changes.push({ path, value });\n          }\n        }\n        if (changes.length > 0) {\n          onStateChangeRef.value?.(changes);\n        }\n      }\n    };\n\n    provide<StateContextValue>(STATE_KEY, {\n      state,\n      get,\n      set,\n      update,\n      getSnapshot,\n    });\n\n    return () => slots.default?.();\n  },\n});\n\n/**\n * Composable to access the state context\n */\nexport function useStateStore(): StateContextValue {\n  const ctx = inject<StateContextValue>(STATE_KEY);\n  if (!ctx) {\n    throw new Error(\"useStateStore must be used within a StateProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Composable to get a value from the state model (reactive)\n */\nexport function useStateValue<T>(path: string): ComputedRef<T | undefined> {\n  const { state } = useStateStore();\n  return computed(() => getByPath(state.value, path) as T | undefined);\n}\n\n/**\n * Composable to get and set a value from the state model by path.\n *\n * This is the path-based variant for use in arbitrary composables. For\n * registry components that receive `bindings` from the renderer, prefer\n * `useBoundProp` which reads the already-resolved prop value and writes back\n * to the bound path.\n */\nexport function useStateBinding<T>(\n  path: string,\n): [ComputedRef<T | undefined>, (value: T) => void] {\n  const { state, set } = useStateStore();\n  const value = computed(() => getByPath(state.value, path) as T | undefined);\n  const setValue = (newValue: T) => set(path, newValue);\n  return [value, setValue];\n}\n"
  },
  {
    "path": "packages/vue/src/composables/validation.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport { StateProvider } from \"./state\";\nimport {\n  ValidationProvider,\n  useOptionalValidation,\n  useValidation,\n  useFieldValidation,\n} from \"./validation\";\n\n/** Mount StateProvider → ValidationProvider with a child that captures context. */\nfunction withProviders<T>(\n  composable: () => T,\n  initialState: Record<string, unknown> = {},\n): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: { initialState } as any,\n    slots: {\n      default: () =>\n        h(ValidationProvider as Component, null, {\n          default: () => h(Child),\n        }),\n    },\n  });\n  return { result };\n}\n\n/** Mount only inside StateProvider (no ValidationProvider) */\nfunction withStateOnly<T>(composable: () => T): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: { initialState: {} } as any,\n    slots: { default: () => h(Child) },\n  });\n  return { result };\n}\n\ndescribe(\"ValidationProvider — provide/inject\", () => {\n  it(\"useValidation() throws outside a provider\", () => {\n    const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    expect(() => useValidation()).toThrow(\n      \"useValidation must be used within a ValidationProvider\",\n    );\n    warn.mockRestore();\n  });\n});\n\ndescribe(\"useOptionalValidation\", () => {\n  it(\"returns null outside a ValidationProvider\", () => {\n    const { result } = withStateOnly(() => useOptionalValidation());\n    expect(result).toBeNull();\n  });\n\n  it(\"returns context inside a ValidationProvider\", () => {\n    const { result } = withProviders(() => useOptionalValidation());\n    expect(result).not.toBeNull();\n    expect(typeof result!.validate).toBe(\"function\");\n    expect(typeof result!.validateAll).toBe(\"function\");\n  });\n});\n\ndescribe(\"useFieldValidation — lifecycle\", () => {\n  it(\"after validate(), isValid and errors reflect the result\", () => {\n    const { result } = withProviders(\n      () =>\n        useFieldValidation(\"/name\", {\n          checks: [{ type: \"required\", message: \"Name is required\" }],\n        }),\n      { name: \"\" },\n    );\n    const validationResult = result.validate();\n    expect(validationResult.valid).toBe(false);\n    expect(validationResult.errors).toContain(\"Name is required\");\n  });\n\n  it(\"after validate() with valid value, isValid is true\", () => {\n    const { result } = withProviders(\n      () =>\n        useFieldValidation(\"/name\", {\n          checks: [{ type: \"required\", message: \"Name is required\" }],\n        }),\n      { name: \"Alice\" },\n    );\n    const validationResult = result.validate();\n    expect(validationResult.valid).toBe(true);\n    expect(validationResult.errors).toHaveLength(0);\n  });\n\n  it(\"touch() sets touched: true in the validation context\", () => {\n    let validationCtx!: ReturnType<typeof useValidation>;\n    let fieldCtx!: ReturnType<typeof useFieldValidation>;\n\n    const Child = defineComponent({\n      setup() {\n        validationCtx = useValidation();\n        fieldCtx = useFieldValidation(\"/email\");\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: {} } as any,\n      slots: {\n        default: () =>\n          h(ValidationProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    fieldCtx.touch();\n    expect(validationCtx.fieldStates[\"/email\"]?.touched).toBe(true);\n  });\n\n  it(\"clear() resets the field state from the validation context\", () => {\n    let validationCtx!: ReturnType<typeof useValidation>;\n    let fieldCtx!: ReturnType<typeof useFieldValidation>;\n\n    const Child = defineComponent({\n      setup() {\n        validationCtx = useValidation();\n        fieldCtx = useFieldValidation(\"/email\", {\n          checks: [{ type: \"required\", message: \"Required\" }],\n        });\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { email: \"\" } } as any,\n      slots: {\n        default: () =>\n          h(ValidationProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    fieldCtx.validate(); // populate fieldStates\n    fieldCtx.clear();\n    expect(validationCtx.fieldStates[\"/email\"]).toBeUndefined();\n  });\n\n  it(\"validateAll() returns true when all registered fields pass\", () => {\n    let validationCtx!: ReturnType<typeof useValidation>;\n\n    const Child = defineComponent({\n      setup() {\n        validationCtx = useValidation();\n        useFieldValidation(\"/name\", {\n          checks: [{ type: \"required\", message: \"Required\" }],\n        });\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { name: \"Alice\" } } as any,\n      slots: {\n        default: () =>\n          h(ValidationProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    expect(validationCtx.validateAll()).toBe(true);\n  });\n\n  it(\"validateAll() returns false when any field fails\", () => {\n    let validationCtx!: ReturnType<typeof useValidation>;\n\n    const Child = defineComponent({\n      setup() {\n        validationCtx = useValidation();\n        useFieldValidation(\"/name\", {\n          checks: [{ type: \"required\", message: \"Required\" }],\n        });\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { name: \"\" } } as any,\n      slots: {\n        default: () =>\n          h(ValidationProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    expect(validationCtx.validateAll()).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/composables/validation.ts",
    "content": "import {\n  computed,\n  defineComponent,\n  inject,\n  onMounted,\n  onUnmounted,\n  provide,\n  ref,\n  type ComputedRef,\n  type PropType,\n} from \"vue\";\nimport {\n  runValidation,\n  type ValidationConfig,\n  type ValidationFunction,\n  type ValidationResult,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Field validation state\n */\nexport interface FieldValidationState {\n  touched: boolean;\n  validated: boolean;\n  result: ValidationResult | null;\n}\n\n/**\n * Validation context value\n */\nexport interface ValidationContextValue {\n  customFunctions: Record<string, ValidationFunction>;\n  fieldStates: Record<string, FieldValidationState>;\n  validate: (path: string, config: ValidationConfig) => ValidationResult;\n  touch: (path: string) => void;\n  clear: (path: string) => void;\n  validateAll: () => boolean;\n  registerField: (path: string, config: ValidationConfig) => void;\n}\n\nconst VALIDATION_KEY = Symbol(\"json-render:validation\");\n\nexport interface ValidationProviderProps {\n  customFunctions?: Record<string, ValidationFunction>;\n}\n\n/**\n * Compare two args records shallowly.\n */\nfunction dynamicArgsEqual(\n  a: Record<string, unknown> | undefined,\n  b: Record<string, unknown> | undefined,\n): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n  const keysA = Object.keys(a);\n  const keysB = Object.keys(b);\n  if (keysA.length !== keysB.length) return false;\n  for (const key of keysA) {\n    const va = a[key];\n    const vb = b[key];\n    if (va === vb) continue;\n    if (\n      typeof va === \"object\" &&\n      va !== null &&\n      typeof vb === \"object\" &&\n      vb !== null\n    ) {\n      const sa = (va as Record<string, unknown>).$state;\n      const sb = (vb as Record<string, unknown>).$state;\n      if (typeof sa === \"string\" && sa === sb) continue;\n    }\n    return false;\n  }\n  return true;\n}\n\n/**\n * Structural equality check for ValidationConfig.\n */\nfunction validationConfigEqual(\n  a: ValidationConfig,\n  b: ValidationConfig,\n): boolean {\n  if (a === b) return true;\n  if (a.validateOn !== b.validateOn) return false;\n  const ac = a.checks ?? [];\n  const bc = b.checks ?? [];\n  if (ac.length !== bc.length) return false;\n  for (let i = 0; i < ac.length; i++) {\n    const ca = ac[i]!;\n    const cb = bc[i]!;\n    if (ca.type !== cb.type) return false;\n    if (ca.message !== cb.message) return false;\n    if (!dynamicArgsEqual(ca.args, cb.args)) return false;\n  }\n  return true;\n}\n\n/**\n * Provider for validation\n */\nexport const ValidationProvider = defineComponent({\n  name: \"ValidationProvider\",\n  props: {\n    customFunctions: {\n      type: Object as PropType<Record<string, ValidationFunction>>,\n      default: () => ({}),\n    },\n  },\n  setup(props, { slots }) {\n    const { state } = useStateStore();\n\n    const fieldStates = ref<Record<string, FieldValidationState>>({});\n    const fieldConfigs = ref<Record<string, ValidationConfig>>({});\n\n    const registerField = (path: string, config: ValidationConfig) => {\n      const existing = fieldConfigs.value[path];\n      if (existing && validationConfigEqual(existing, config)) return;\n      fieldConfigs.value = { ...fieldConfigs.value, [path]: config };\n    };\n\n    const validate = (\n      path: string,\n      config: ValidationConfig,\n    ): ValidationResult => {\n      const currentState = state.value;\n      const segments = path.split(\"/\").filter(Boolean);\n      let value: unknown = currentState;\n      for (const seg of segments) {\n        if (value != null && typeof value === \"object\") {\n          value = (value as Record<string, unknown>)[seg];\n        } else {\n          value = undefined;\n          break;\n        }\n      }\n      const result = runValidation(config, {\n        value,\n        stateModel: currentState,\n        customFunctions: props.customFunctions,\n      });\n\n      fieldStates.value = {\n        ...fieldStates.value,\n        [path]: {\n          touched: fieldStates.value[path]?.touched ?? true,\n          validated: true,\n          result,\n        },\n      };\n\n      return result;\n    };\n\n    const touch = (path: string) => {\n      fieldStates.value = {\n        ...fieldStates.value,\n        [path]: {\n          ...fieldStates.value[path],\n          touched: true,\n          validated: fieldStates.value[path]?.validated ?? false,\n          result: fieldStates.value[path]?.result ?? null,\n        },\n      };\n    };\n\n    const clear = (path: string) => {\n      const { [path]: _, ...rest } = fieldStates.value;\n      fieldStates.value = rest;\n    };\n\n    const validateAll = (): boolean => {\n      let allValid = true;\n      for (const [path, config] of Object.entries(fieldConfigs.value)) {\n        const result = validate(path, config);\n        if (!result.valid) {\n          allValid = false;\n        }\n      }\n      return allValid;\n    };\n\n    provide<ValidationContextValue>(VALIDATION_KEY, {\n      get customFunctions() {\n        return props.customFunctions;\n      },\n      get fieldStates() {\n        return fieldStates.value;\n      },\n      validate,\n      touch,\n      clear,\n      validateAll,\n      registerField,\n    });\n\n    return () => slots.default?.();\n  },\n});\n\n/**\n * Composable to access validation context (or null if not inside a ValidationProvider).\n * Useful for components that optionally participate in form validation.\n */\nexport function useOptionalValidation(): ValidationContextValue | null {\n  return (\n    inject<ValidationContextValue>(\n      VALIDATION_KEY,\n      null as unknown as ValidationContextValue,\n    ) ?? null\n  );\n}\n\n/**\n * Composable to access validation context\n */\nexport function useValidation(): ValidationContextValue {\n  const ctx = inject<ValidationContextValue>(VALIDATION_KEY);\n  if (!ctx) {\n    throw new Error(\"useValidation must be used within a ValidationProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Composable to get validation state for a field\n */\nexport function useFieldValidation(\n  path: string,\n  config?: ValidationConfig,\n): {\n  state: ComputedRef<FieldValidationState>;\n  validate: () => ValidationResult;\n  touch: () => void;\n  clear: () => void;\n  errors: ComputedRef<string[]>;\n  isValid: ComputedRef<boolean>;\n} {\n  const ctx = useValidation();\n\n  onMounted(() => {\n    if (path && config) {\n      ctx.registerField(path, config);\n    }\n  });\n\n  onUnmounted(() => {\n    ctx.clear(path);\n  });\n\n  const defaultState: FieldValidationState = {\n    touched: false,\n    validated: false,\n    result: null,\n  };\n\n  return {\n    state: computed(() => ctx.fieldStates[path] ?? defaultState),\n    validate: () => ctx.validate(path, config ?? { checks: [] }),\n    touch: () => ctx.touch(path),\n    clear: () => ctx.clear(path),\n    errors: computed(() => ctx.fieldStates[path]?.result?.errors ?? []),\n    isValid: computed(() => ctx.fieldStates[path]?.result?.valid ?? true),\n  };\n}\n"
  },
  {
    "path": "packages/vue/src/composables/visibility.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, type Component, type ComputedRef } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport { StateProvider, useStateStore } from \"./state\";\nimport { VisibilityProvider, useVisibility, useIsVisible } from \"./visibility\";\n\n/** Mount StateProvider → VisibilityProvider with a child that captures context. */\nfunction withProviders<T>(\n  composable: () => T,\n  initialState: Record<string, unknown> = {},\n): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: { initialState } as any,\n    slots: {\n      default: () =>\n        h(VisibilityProvider as Component, null, {\n          default: () => h(Child),\n        }),\n    },\n  });\n  return { result };\n}\n\ndescribe(\"VisibilityProvider — provide/inject\", () => {\n  it(\"useVisibility() throws outside a VisibilityProvider\", () => {\n    const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    expect(() => useVisibility()).toThrow(\n      \"useVisibility must be used within a VisibilityProvider\",\n    );\n    warn.mockRestore();\n  });\n\n  it(\"useVisibility() returns a context with isVisible and ctx\", () => {\n    const { result } = withProviders(() => useVisibility());\n    expect(typeof result.isVisible).toBe(\"function\");\n    expect(result.ctx).toBeDefined();\n    expect(result.ctx.value).toBeDefined();\n  });\n});\n\ndescribe(\"useIsVisible — state integration\", () => {\n  it(\"undefined condition returns true\", () => {\n    const { result } = withProviders(() => useVisibility());\n    expect(result.isVisible(undefined)).toBe(true);\n  });\n\n  it(\"{ $state: '/flag' } returns true when state flag is truthy\", () => {\n    const { result } = withProviders(() => useVisibility(), { flag: true });\n    expect(result.isVisible({ $state: \"/flag\" })).toBe(true);\n  });\n\n  it(\"{ $state: '/flag' } returns false when state flag is falsy\", () => {\n    const { result } = withProviders(() => useVisibility(), { flag: false });\n    expect(result.isVisible({ $state: \"/flag\" })).toBe(false);\n  });\n\n  it(\"ctx is a ComputedRef whose .value reflects current state\", () => {\n    const { result } = withProviders(() => useVisibility(), { count: 3 });\n    expect(result.ctx.value.stateModel).toEqual({ count: 3 });\n  });\n});\n\ndescribe(\"useIsVisible — reactivity\", () => {\n  it(\"returns a ComputedRef<boolean> that updates when state changes\", () => {\n    let storeCtx!: ReturnType<typeof useStateStore>;\n    let isVisible!: ComputedRef<boolean>;\n\n    const Child = defineComponent({\n      setup() {\n        storeCtx = useStateStore();\n        isVisible = useIsVisible({ $state: \"/flag\" });\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { flag: false } } as any,\n      slots: {\n        default: () =>\n          h(VisibilityProvider as Component, null, {\n            default: () => h(Child),\n          }),\n      },\n    });\n\n    expect(isVisible.value).toBe(false);\n    storeCtx.set(\"/flag\", true);\n    expect(isVisible.value).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/composables/visibility.ts",
    "content": "import {\n  computed,\n  defineComponent,\n  inject,\n  provide,\n  type ComputedRef,\n} from \"vue\";\nimport {\n  evaluateVisibility,\n  type VisibilityCondition,\n  type VisibilityContext as CoreVisibilityContext,\n} from \"@json-render/core\";\nimport { useStateStore } from \"./state\";\n\n/**\n * Visibility context value\n */\nexport interface VisibilityContextValue {\n  /** Evaluate a visibility condition */\n  isVisible: (condition: VisibilityCondition | undefined) => boolean;\n  /** The underlying visibility context (reactive) */\n  ctx: ComputedRef<CoreVisibilityContext>;\n}\n\nconst VISIBILITY_KEY = Symbol(\"json-render:visibility\");\n\n/**\n * Provider for visibility evaluation\n */\nexport const VisibilityProvider = defineComponent({\n  name: \"VisibilityProvider\",\n  setup(_, { slots }) {\n    const { state } = useStateStore();\n\n    const ctx = computed<CoreVisibilityContext>(() => ({\n      stateModel: state.value,\n    }));\n\n    const isVisible = (condition: VisibilityCondition | undefined): boolean =>\n      evaluateVisibility(condition, ctx.value);\n\n    provide<VisibilityContextValue>(VISIBILITY_KEY, { isVisible, ctx });\n\n    return () => slots.default?.();\n  },\n});\n\n/**\n * Composable to access visibility evaluation\n */\nexport function useVisibility(): VisibilityContextValue {\n  const ctx = inject<VisibilityContextValue>(VISIBILITY_KEY);\n  if (!ctx) {\n    throw new Error(\"useVisibility must be used within a VisibilityProvider\");\n  }\n  return ctx;\n}\n\n/**\n * Composable to check if a condition is visible. Returns a reactive\n * `ComputedRef<boolean>` so the result updates whenever state changes.\n */\nexport function useIsVisible(\n  condition: VisibilityCondition | undefined,\n): ComputedRef<boolean> {\n  const { ctx } = useVisibility();\n  return computed(() => evaluateVisibility(condition, ctx.value));\n}\n"
  },
  {
    "path": "packages/vue/src/dynamic-forms.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, nextTick, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport type { Spec } from \"@json-render/core\";\nimport { StateProvider, useStateStore } from \"./composables/state\";\nimport { VisibilityProvider } from \"./composables/visibility\";\nimport { ActionProvider } from \"./composables/actions\";\nimport { ValidationProvider } from \"./composables/validation\";\nimport { useFieldValidation } from \"./composables/validation\";\nimport { useBoundProp } from \"./hooks\";\nimport {\n  Renderer,\n  JSONUIProvider,\n  type ComponentRegistry,\n  type ComponentRenderProps,\n} from \"./renderer\";\n\n// =============================================================================\n// Stub components\n// =============================================================================\n\nconst Button = defineComponent({\n  name: \"Button\",\n  props: {\n    element: { type: Object, required: true },\n    emit: { type: Function, required: true },\n    on: { type: Function, required: true },\n    bindings: { type: Object, default: undefined },\n    loading: { type: Boolean, default: undefined },\n  },\n  setup(props) {\n    return () =>\n      h(\n        \"button\",\n        { \"data-testid\": \"btn\", onClick: () => props.emit(\"press\") },\n        String((props.element as any).props?.label ?? \"\"),\n      );\n  },\n});\n\nconst Text = defineComponent({\n  name: \"Text\",\n  props: {\n    element: { type: Object, required: true },\n    emit: { type: Function, required: true },\n    on: { type: Function, required: true },\n    bindings: { type: Object, default: undefined },\n    loading: { type: Boolean, default: undefined },\n  },\n  setup(props) {\n    return () => {\n      const value = (props.element as any).props?.text;\n      return h(\n        \"span\",\n        { \"data-testid\": \"text\" },\n        value == null\n          ? \"\"\n          : typeof value === \"string\"\n            ? value\n            : JSON.stringify(value),\n      );\n    };\n  },\n});\n\nconst Stack = defineComponent({\n  name: \"Stack\",\n  props: {\n    element: { type: Object, required: true },\n    emit: { type: Function, required: true },\n    on: { type: Function, required: true },\n    bindings: { type: Object, default: undefined },\n    loading: { type: Boolean, default: undefined },\n  },\n  setup(_props, { slots }) {\n    return () => h(\"div\", { \"data-testid\": \"stack\" }, slots.default?.());\n  },\n});\n\nconst InputField = defineComponent({\n  name: \"InputField\",\n  props: {\n    element: { type: Object, required: true },\n    emit: { type: Function, required: true },\n    on: { type: Function, required: true },\n    bindings: { type: Object, default: undefined },\n    loading: { type: Boolean, default: undefined },\n  },\n  setup(props) {\n    const elProps = (props.element as any).props ?? {};\n    const bindingPath = (props.bindings as Record<string, string>)?.value;\n    const [boundValue, setBoundValue] = useBoundProp<string>(\n      elProps.value as string | undefined,\n      bindingPath,\n    );\n\n    const hasValidation = !!(bindingPath && elProps.checks?.length);\n    const config = hasValidation ? { checks: elProps.checks ?? [] } : undefined;\n    const { errors } = useFieldValidation(bindingPath ?? \"\", config);\n\n    return () =>\n      h(\"div\", null, [\n        elProps.label ? h(\"label\", null, elProps.label) : null,\n        h(\"input\", {\n          \"data-testid\": \"input\",\n          value: boundValue ?? \"\",\n          onInput: (e: Event) =>\n            setBoundValue((e.target as HTMLInputElement).value),\n        }),\n        errors.value.length > 0\n          ? h(\"span\", { \"data-testid\": \"input-error\" }, errors.value[0])\n          : null,\n      ]);\n  },\n});\n\nconst Select = defineComponent({\n  name: \"Select\",\n  props: {\n    element: { type: Object, required: true },\n    emit: { type: Function, required: true },\n    on: { type: Function, required: true },\n    bindings: { type: Object, default: undefined },\n    loading: { type: Boolean, default: undefined },\n  },\n  setup(props) {\n    const elProps = (props.element as any).props ?? {};\n    const bindingPath = (props.bindings as Record<string, string>)?.value;\n    const [boundValue] = useBoundProp<string>(\n      elProps.value as string | undefined,\n      bindingPath,\n    );\n    return () => h(\"span\", { \"data-testid\": \"select-value\" }, boundValue ?? \"\");\n  },\n});\n\nconst registry: ComponentRegistry = {\n  Button: Button as unknown as Component,\n  Text: Text as unknown as Component,\n  Input: InputField as unknown as Component,\n  Select: Select as unknown as Component,\n  Stack: Stack as unknown as Component,\n};\n\n// State probe to read state from tests\nconst StateProbe = defineComponent({\n  name: \"StateProbe\",\n  setup() {\n    const { state } = useStateStore();\n    return () =>\n      h(\"pre\", { \"data-testid\": \"state-probe\" }, JSON.stringify(state.value));\n  },\n});\n\nfunction getState(wrapper: ReturnType<typeof mount>): Record<string, unknown> {\n  return JSON.parse(wrapper.find(\"[data-testid='state-probe']\").text());\n}\n\n// =============================================================================\n// Mount helper\n// =============================================================================\n\nfunction mountWithProviders(\n  spec: Spec,\n  opts: {\n    functions?: Record<string, (args: Record<string, unknown>) => unknown>;\n    handlers?: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void> | void\n    >;\n    initialState?: Record<string, unknown>;\n  } = {},\n) {\n  return mount(JSONUIProvider as Component, {\n    props: {\n      registry,\n      initialState: opts.initialState ?? spec.state ?? {},\n      functions: opts.functions,\n      handlers: opts.handlers,\n    } as any,\n    slots: {\n      default: () => [h(Renderer, { spec, registry }), h(StateProbe)],\n    },\n  });\n}\n\n// =============================================================================\n// $computed expressions in rendering\n// =============================================================================\n\ndescribe(\"$computed expressions in rendering\", () => {\n  it(\"resolves a $computed prop using provided functions\", () => {\n    const spec: Spec = {\n      state: { first: \"Jane\", last: \"Doe\" },\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: {\n              $computed: \"fullName\",\n              args: {\n                first: { $state: \"/first\" },\n                last: { $state: \"/last\" },\n              },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    const functions = {\n      fullName: (args: Record<string, unknown>) => `${args.first} ${args.last}`,\n    };\n\n    const wrapper = mountWithProviders(spec, { functions });\n    expect(wrapper.find(\"[data-testid='text']\").text()).toBe(\"Jane Doe\");\n  });\n\n  it(\"renders gracefully when functions prop is omitted\", () => {\n    const warnSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n    const spec: Spec = {\n      state: {},\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Text\",\n          props: {\n            text: { $computed: \"missing\" },\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec);\n    expect(wrapper.find(\"[data-testid='text']\").text()).toBe(\"\");\n    warnSpy.mockRestore();\n  });\n});\n\n// =============================================================================\n// Watchers\n// =============================================================================\n\ndescribe(\"watchers (watch field)\", () => {\n  it(\"does not fire on initial render, fires when watched state changes\", async () => {\n    const loadCities = vi.fn();\n\n    const spec: Spec = {\n      state: { form: { country: \"\" }, citiesLoaded: false },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Set Country\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/form/country\", value: \"US\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Select\",\n          props: { value: { $state: \"/form/country\" } },\n          watch: {\n            \"/form/country\": {\n              action: \"loadCities\",\n              params: { country: { $state: \"/form/country\" } },\n            },\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec, { handlers: { loadCities } });\n\n    expect(loadCities).not.toHaveBeenCalled();\n\n    await wrapper.find(\"[data-testid='btn']\").trigger(\"click\");\n    await nextTick();\n    await nextTick();\n\n    expect(loadCities).toHaveBeenCalledTimes(1);\n    expect(loadCities).toHaveBeenCalledWith(\n      expect.objectContaining({ country: \"US\" }),\n    );\n  });\n\n  it(\"fires multiple action bindings on the same watch path\", async () => {\n    const action1 = vi.fn();\n    const action2 = vi.fn();\n\n    const spec: Spec = {\n      state: { value: \"a\" },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"btn\", \"watcher\"],\n        },\n        btn: {\n          type: \"Button\",\n          props: { label: \"Change\" },\n          on: {\n            press: [\n              {\n                action: \"setState\",\n                params: { statePath: \"/value\", value: \"b\" },\n              },\n            ],\n          },\n          children: [],\n        },\n        watcher: {\n          type: \"Text\",\n          props: { text: { $state: \"/value\" } },\n          watch: {\n            \"/value\": [{ action: \"action1\" }, { action: \"action2\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec, {\n      handlers: { action1, action2 },\n    });\n\n    await wrapper.find(\"[data-testid='btn']\").trigger(\"click\");\n    await nextTick();\n    await nextTick();\n\n    expect(action1).toHaveBeenCalledTimes(1);\n    expect(action2).toHaveBeenCalledTimes(1);\n  });\n});\n\n// =============================================================================\n// validateForm action\n// =============================================================================\n\ndescribe(\"validateForm action\", () => {\n  it(\"writes { valid: false, errors } when a required field is empty\", async () => {\n    const spec: Spec = {\n      state: { form: { email: \"\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec);\n    await wrapper.find(\"[data-testid='btn']\").trigger(\"click\");\n    await nextTick();\n\n    const state = getState(wrapper);\n    expect(state.result).toEqual({\n      valid: false,\n      errors: { \"/form/email\": [\"Email is required\"] },\n    });\n  });\n\n  it(\"writes { valid: true, errors: {} } when all fields pass validation\", async () => {\n    const spec: Spec = {\n      state: { form: { email: \"test@example.com\" }, result: null },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"emailInput\", \"submitBtn\"],\n        },\n        emailInput: {\n          type: \"Input\",\n          props: {\n            label: \"Email\",\n            value: { $bindState: \"/form/email\" },\n            checks: [{ type: \"required\", message: \"Email is required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [\n              {\n                action: \"validateForm\",\n                params: { statePath: \"/result\" },\n              },\n            ],\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec);\n    await wrapper.find(\"[data-testid='btn']\").trigger(\"click\");\n    await nextTick();\n\n    const state = getState(wrapper);\n    expect(state.result).toEqual({ valid: true, errors: {} });\n  });\n\n  it(\"defaults to /formValidation when no statePath is provided\", async () => {\n    const spec: Spec = {\n      state: { form: { name: \"filled\" } },\n      root: \"wrapper\",\n      elements: {\n        wrapper: {\n          type: \"Stack\",\n          props: {},\n          children: [\"nameInput\", \"submitBtn\"],\n        },\n        nameInput: {\n          type: \"Input\",\n          props: {\n            label: \"Name\",\n            value: { $bindState: \"/form/name\" },\n            checks: [{ type: \"required\", message: \"Required\" }],\n          },\n          children: [],\n        },\n        submitBtn: {\n          type: \"Button\",\n          props: { label: \"Submit\" },\n          on: {\n            press: [{ action: \"validateForm\" }],\n          },\n          children: [],\n        },\n      },\n    };\n\n    const wrapper = mountWithProviders(spec);\n    await wrapper.find(\"[data-testid='btn']\").trigger(\"click\");\n    await nextTick();\n\n    const state = getState(wrapper);\n    expect(state.formValidation).toEqual({ valid: true, errors: {} });\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/hooks.test.ts",
    "content": "import { describe, it, expect, vi, afterEach } from \"vitest\";\nimport { defineComponent, h, ref, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport { SPEC_DATA_PART_TYPE } from \"@json-render/core\";\nimport { StateProvider, useStateStore } from \"./composables/state\";\nimport {\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n  useBoundProp,\n  useUIStream,\n  useJsonRenderMessage,\n  type DataPart,\n} from \"./hooks\";\n\n// ---------------------------------------------------------------------------\n// Test helpers\n// ---------------------------------------------------------------------------\n\n/** Mount inside a StateProvider and capture a composable's result. */\nfunction withStateProvider<T>(\n  composable: () => T,\n  initialState: Record<string, unknown> = {},\n): { result: T } {\n  let result!: T;\n  const Child = defineComponent({\n    setup() {\n      result = composable();\n      return () => h(\"div\");\n    },\n  });\n  mount(StateProvider as Component, {\n    props: { initialState } as any,\n    slots: { default: () => h(Child) },\n  });\n  return { result };\n}\n\n/** Create a simple ReadableStream from a string (for fetch mocks). */\nfunction makeReadableStream(text: string): ReadableStream<Uint8Array> {\n  const encoder = new TextEncoder();\n  return new ReadableStream({\n    start(controller) {\n      controller.enqueue(encoder.encode(text));\n      controller.close();\n    },\n  });\n}\n\n// ---------------------------------------------------------------------------\n// flatToTree\n// ---------------------------------------------------------------------------\n\ndescribe(\"flatToTree\", () => {\n  it(\"converts flat elements into a Spec with root and children\", () => {\n    const elements = [\n      { key: \"root\", type: \"Stack\", props: {}, parentKey: undefined },\n      {\n        key: \"child1\",\n        type: \"Text\",\n        props: { content: \"hello\" },\n        parentKey: \"root\",\n      },\n    ];\n    const spec = flatToTree(elements as any);\n    expect(spec.root).toBe(\"root\");\n    expect(spec.elements[\"root\"]?.children).toContain(\"child1\");\n    expect(spec.elements[\"child1\"]).toBeDefined();\n  });\n\n  it(\"handles a single root element with no children\", () => {\n    const elements = [\n      { key: \"root\", type: \"Text\", props: {}, parentKey: undefined },\n    ];\n    const spec = flatToTree(elements as any);\n    expect(spec.root).toBe(\"root\");\n    expect(spec.elements[\"root\"]?.children).toEqual([]);\n  });\n});\n\n// ---------------------------------------------------------------------------\n// buildSpecFromParts\n// ---------------------------------------------------------------------------\n\ndescribe(\"buildSpecFromParts\", () => {\n  it(\"returns null when no spec parts present\", () => {\n    const parts: DataPart[] = [{ type: \"text\", text: \"hello\" }];\n    expect(buildSpecFromParts(parts)).toBeNull();\n  });\n\n  it(\"returns null for empty array\", () => {\n    expect(buildSpecFromParts([])).toBeNull();\n  });\n\n  it(\"returns a Spec when a snapshot spec data part is present\", () => {\n    const part: DataPart = {\n      type: SPEC_DATA_PART_TYPE,\n      data: {\n        type: \"flat\",\n        spec: {\n          root: \"r\",\n          elements: { r: { type: \"Text\", props: { content: \"hi\" } } },\n        },\n      },\n    };\n    const result = buildSpecFromParts([part]);\n    expect(result?.root).toBe(\"r\");\n    expect(result?.elements[\"r\"]).toBeDefined();\n  });\n\n  it(\"applies patch operations incrementally\", () => {\n    const parts: DataPart[] = [\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"patch\",\n          patch: { op: \"add\", path: \"/root\", value: \"myRoot\" },\n        },\n      },\n    ];\n    const result = buildSpecFromParts(parts);\n    expect(result?.root).toBe(\"myRoot\");\n  });\n\n  it(\"skips malformed data parts silently\", () => {\n    const parts: DataPart[] = [\n      { type: SPEC_DATA_PART_TYPE, data: { type: \"unknown\" } },\n    ];\n    expect(buildSpecFromParts(parts)).toBeNull();\n  });\n});\n\n// ---------------------------------------------------------------------------\n// getTextFromParts\n// ---------------------------------------------------------------------------\n\ndescribe(\"getTextFromParts\", () => {\n  it(\"concatenates text parts with double newlines\", () => {\n    const parts: DataPart[] = [\n      { type: \"text\", text: \"hello\" },\n      { type: \"text\", text: \"world\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"hello\\n\\nworld\");\n  });\n\n  it(\"ignores non-text parts\", () => {\n    const parts: DataPart[] = [\n      { type: \"other\", data: {} },\n      { type: \"text\", text: \"hi\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"hi\");\n  });\n\n  it(\"returns empty string for empty array\", () => {\n    expect(getTextFromParts([])).toBe(\"\");\n  });\n\n  it(\"filters out empty/whitespace text parts\", () => {\n    const parts: DataPart[] = [\n      { type: \"text\", text: \"  \" },\n      { type: \"text\", text: \"real\" },\n    ];\n    expect(getTextFromParts(parts)).toBe(\"real\");\n  });\n});\n\n// ---------------------------------------------------------------------------\n// useBoundProp\n// ---------------------------------------------------------------------------\n\ndescribe(\"useBoundProp\", () => {\n  it(\"returns the prop value and a no-op setter when no binding path\", () => {\n    const { result } = withStateProvider(() =>\n      useBoundProp(\"hello\", undefined),\n    );\n    const [value, setValue] = result;\n    expect(value).toBe(\"hello\");\n    expect(() => setValue(\"new\")).not.toThrow();\n  });\n\n  it(\"returns undefined prop value when propValue is undefined\", () => {\n    const { result } = withStateProvider(() =>\n      useBoundProp<string>(undefined, undefined),\n    );\n    expect(result[0]).toBeUndefined();\n  });\n\n  it(\"setter writes to state at the binding path\", () => {\n    let storeCtx!: ReturnType<typeof useStateStore>;\n    let setter!: (v: string) => void;\n\n    const Child = defineComponent({\n      setup() {\n        storeCtx = useStateStore();\n        const [, s] = useBoundProp<string>(\"Alice\", \"/name\");\n        setter = s;\n        return () => h(\"div\");\n      },\n    });\n\n    mount(StateProvider as Component, {\n      props: { initialState: { name: \"Alice\" } } as any,\n      slots: { default: () => h(Child) },\n    });\n\n    setter(\"Bob\");\n    expect(storeCtx.get(\"/name\")).toBe(\"Bob\");\n  });\n});\n\n// ---------------------------------------------------------------------------\n// useUIStream\n// ---------------------------------------------------------------------------\n\ndescribe(\"useUIStream\", () => {\n  afterEach(() => {\n    vi.unstubAllGlobals();\n  });\n\n  it(\"send() sets isStreaming true then false after completion\", async () => {\n    const patchLine =\n      JSON.stringify({ op: \"add\", path: \"/root\", value: \"myRoot\" }) +\n      \"\\n\" +\n      JSON.stringify({\n        op: \"add\",\n        path: \"/elements/myRoot\",\n        value: { type: \"Text\", props: {} },\n      }) +\n      \"\\n\";\n\n    vi.stubGlobal(\n      \"fetch\",\n      vi.fn().mockResolvedValue({\n        ok: true,\n        body: makeReadableStream(patchLine),\n      }),\n    );\n\n    let streamResult!: ReturnType<typeof useUIStream>;\n    const Child = defineComponent({\n      setup() {\n        streamResult = useUIStream({ api: \"/api/ui\" });\n        return () => h(\"div\");\n      },\n    });\n    mount(StateProvider as Component, {\n      props: { initialState: {} } as any,\n      slots: { default: () => h(Child) },\n    });\n\n    expect(streamResult.isStreaming.value).toBe(false);\n    const promise = streamResult.send(\"build me a UI\");\n    expect(streamResult.isStreaming.value).toBe(true);\n    await promise;\n    expect(streamResult.isStreaming.value).toBe(false);\n    expect(streamResult.spec.value?.root).toBe(\"myRoot\");\n  });\n\n  it(\"error is set when fetch fails\", async () => {\n    vi.stubGlobal(\n      \"fetch\",\n      vi.fn().mockRejectedValue(new Error(\"Network error\")),\n    );\n\n    let streamResult!: ReturnType<typeof useUIStream>;\n    const Child = defineComponent({\n      setup() {\n        streamResult = useUIStream({ api: \"/api/ui\" });\n        return () => h(\"div\");\n      },\n    });\n    mount(StateProvider as Component, {\n      props: { initialState: {} } as any,\n      slots: { default: () => h(Child) },\n    });\n\n    await streamResult.send(\"fail\");\n    expect(streamResult.error.value?.message).toBe(\"Network error\");\n  });\n\n  it(\"clear() resets spec and error\", async () => {\n    vi.stubGlobal(\"fetch\", vi.fn().mockRejectedValue(new Error(\"oops\")));\n\n    let streamResult!: ReturnType<typeof useUIStream>;\n    const Child = defineComponent({\n      setup() {\n        streamResult = useUIStream({ api: \"/api/ui\" });\n        return () => h(\"div\");\n      },\n    });\n    mount(StateProvider as Component, {\n      props: { initialState: {} } as any,\n      slots: { default: () => h(Child) },\n    });\n\n    await streamResult.send(\"fail\");\n    expect(streamResult.error.value).not.toBeNull();\n    streamResult.clear();\n    expect(streamResult.error.value).toBeNull();\n    expect(streamResult.spec.value).toBeNull();\n  });\n});\n\n// ---------------------------------------------------------------------------\n// useJsonRenderMessage\n// ---------------------------------------------------------------------------\n\ndescribe(\"useJsonRenderMessage\", () => {\n  it(\"returns null spec and false hasSpec when no spec parts\", () => {\n    const { spec, hasSpec } = useJsonRenderMessage([\n      { type: \"text\", text: \"hi\" },\n    ]);\n    expect(spec.value).toBeNull();\n    expect(hasSpec.value).toBe(false);\n  });\n\n  it(\"extracts text from text parts\", () => {\n    const { text } = useJsonRenderMessage([\n      { type: \"text\", text: \"hello\" },\n      { type: \"text\", text: \"world\" },\n    ]);\n    expect(text.value).toBe(\"hello\\n\\nworld\");\n  });\n\n  it(\"is reactive when passed a Ref<DataPart[]>\", () => {\n    const parts = ref<DataPart[]>([]);\n    const { text, spec, hasSpec } = useJsonRenderMessage(parts);\n    expect(text.value).toBe(\"\");\n    expect(spec.value).toBeNull();\n\n    parts.value = [{ type: \"text\", text: \"hello\" }];\n    expect(text.value).toBe(\"hello\");\n\n    parts.value = [\n      ...parts.value,\n      {\n        type: SPEC_DATA_PART_TYPE,\n        data: {\n          type: \"flat\",\n          spec: { root: \"r\", elements: { r: { type: \"Text\", props: {} } } },\n        },\n      },\n    ];\n    expect(spec.value?.root).toBe(\"r\");\n    expect(hasSpec.value).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/hooks.ts",
    "content": "import {\n  ref,\n  shallowRef,\n  computed,\n  onUnmounted,\n  isRef,\n  type Ref,\n  type ComputedRef,\n} from \"vue\";\nimport { useStateStore } from \"./composables/state\";\nimport type {\n  Spec,\n  UIElement,\n  FlatElement,\n  JsonPatch,\n  SpecDataPart,\n} from \"@json-render/core\";\nimport {\n  setByPath,\n  getByPath,\n  addByPath,\n  removeByPath,\n  createMixedStreamParser,\n  applySpecPatch,\n  nestedToFlat,\n  SPEC_DATA_PART_TYPE,\n} from \"@json-render/core\";\n\n/**\n * Token usage metadata from AI generation\n */\nexport interface TokenUsage {\n  promptTokens: number;\n  completionTokens: number;\n  totalTokens: number;\n}\n\n/**\n * Parse result for a single line -- either a patch or usage metadata\n */\ntype ParsedLine =\n  | { type: \"patch\"; patch: JsonPatch }\n  | { type: \"usage\"; usage: TokenUsage }\n  | null;\n\n/**\n * Parse a single JSON line (patch or metadata)\n */\nfunction parseLine(line: string): ParsedLine {\n  try {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"//\")) {\n      return null;\n    }\n    const parsed = JSON.parse(trimmed);\n\n    // Check for usage metadata\n    if (parsed.__meta === \"usage\") {\n      return {\n        type: \"usage\",\n        usage: {\n          promptTokens: parsed.promptTokens ?? 0,\n          completionTokens: parsed.completionTokens ?? 0,\n          totalTokens: parsed.totalTokens ?? 0,\n        },\n      };\n    }\n\n    return { type: \"patch\", patch: parsed as JsonPatch };\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Set a value at a spec path (for add/replace operations).\n */\nfunction setSpecValue(newSpec: Spec, path: string, value: unknown): void {\n  if (path === \"/root\") {\n    newSpec.root = value as string;\n    return;\n  }\n\n  if (path === \"/state\") {\n    newSpec.state = value as Record<string, unknown>;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\")) {\n    if (!newSpec.state) newSpec.state = {};\n    const statePath = path.slice(\"/state\".length); // e.g. \"/posts\"\n    setByPath(newSpec.state as Record<string, unknown>, statePath, value);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      newSpec.elements[elementKey] = value as UIElement;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        setByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n          value,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Remove a value at a spec path.\n */\nfunction removeSpecValue(newSpec: Spec, path: string): void {\n  if (path === \"/state\") {\n    delete newSpec.state;\n    return;\n  }\n\n  if (path.startsWith(\"/state/\") && newSpec.state) {\n    const statePath = path.slice(\"/state\".length);\n    removeByPath(newSpec.state as Record<string, unknown>, statePath);\n    return;\n  }\n\n  if (path.startsWith(\"/elements/\")) {\n    const pathParts = path.slice(\"/elements/\".length).split(\"/\");\n    const elementKey = pathParts[0];\n    if (!elementKey) return;\n\n    if (pathParts.length === 1) {\n      const { [elementKey]: _, ...rest } = newSpec.elements;\n      newSpec.elements = rest;\n    } else {\n      const element = newSpec.elements[elementKey];\n      if (element) {\n        const propPath = \"/\" + pathParts.slice(1).join(\"/\");\n        const newElement = { ...element };\n        removeByPath(\n          newElement as unknown as Record<string, unknown>,\n          propPath,\n        );\n        newSpec.elements[elementKey] = newElement;\n      }\n    }\n  }\n}\n\n/**\n * Get a value at a spec path.\n */\nfunction getSpecValue(spec: Spec, path: string): unknown {\n  if (path === \"/root\") return spec.root;\n  if (path === \"/state\") return spec.state;\n  if (path.startsWith(\"/state/\") && spec.state) {\n    const statePath = path.slice(\"/state\".length);\n    return getByPath(spec.state as Record<string, unknown>, statePath);\n  }\n  return getByPath(spec as unknown as Record<string, unknown>, path);\n}\n\n/**\n * Apply an RFC 6902 JSON patch to the current spec.\n * Supports add, remove, replace, move, copy, and test operations.\n */\nfunction applyPatch(spec: Spec, patch: JsonPatch): Spec {\n  const newSpec = {\n    ...spec,\n    elements: { ...spec.elements },\n    ...(spec.state ? { state: { ...spec.state } } : {}),\n  };\n\n  switch (patch.op) {\n    case \"add\":\n    case \"replace\": {\n      setSpecValue(newSpec, patch.path, patch.value);\n      break;\n    }\n    case \"remove\": {\n      removeSpecValue(newSpec, patch.path);\n      break;\n    }\n    case \"move\": {\n      if (!patch.from) break;\n      const moveValue = getSpecValue(newSpec, patch.from);\n      removeSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, moveValue);\n      break;\n    }\n    case \"copy\": {\n      if (!patch.from) break;\n      const copyValue = getSpecValue(newSpec, patch.from);\n      setSpecValue(newSpec, patch.path, copyValue);\n      break;\n    }\n    case \"test\": {\n      // test is a no-op for rendering purposes (validation only)\n      break;\n    }\n  }\n\n  return newSpec;\n}\n\n/**\n * Options for useUIStream\n */\nexport interface UseUIStreamOptions {\n  /** API endpoint */\n  api: string;\n  /** Callback when complete */\n  onComplete?: (spec: Spec) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useUIStream\n */\nexport interface UseUIStreamReturn {\n  /** Current UI spec */\n  spec: Ref<Spec | null>;\n  /** Whether currently streaming */\n  isStreaming: Ref<boolean>;\n  /** Error if any */\n  error: Ref<Error | null>;\n  /** Token usage from the last generation */\n  usage: Ref<TokenUsage | null>;\n  /** Raw JSONL lines received from the stream (JSON patch lines) */\n  rawLines: Ref<string[]>;\n  /** Send a prompt to generate UI */\n  send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;\n  /** Clear the current spec */\n  clear: () => void;\n}\n\n/**\n * Composable for streaming UI generation\n */\nexport function useUIStream({\n  api,\n  onComplete,\n  onError,\n}: UseUIStreamOptions): UseUIStreamReturn {\n  const spec = shallowRef<Spec | null>(null);\n  const isStreaming = ref(false);\n  const error = ref<Error | null>(null);\n  const usage = ref<TokenUsage | null>(null);\n  const rawLines = ref<string[]>([]);\n\n  const onCompleteRef = ref(onComplete);\n  const onErrorRef = ref(onError);\n\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    spec.value = null;\n    error.value = null;\n    usage.value = null;\n    rawLines.value = [];\n  };\n\n  const send = async (\n    prompt: string,\n    context?: Record<string, unknown>,\n  ): Promise<void> => {\n    // Abort any existing request\n    abortController?.abort();\n    abortController = new AbortController();\n\n    isStreaming.value = true;\n    error.value = null;\n    usage.value = null;\n    rawLines.value = [];\n\n    // Start with previous spec if provided, otherwise empty spec\n    const previousSpec = context?.previousSpec as Spec | undefined;\n    let currentSpec: Spec =\n      previousSpec && previousSpec.root\n        ? { ...previousSpec, elements: { ...previousSpec.elements } }\n        : { root: \"\", elements: {} };\n    spec.value = currentSpec;\n\n    try {\n      const response = await fetch(api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          prompt,\n          context,\n          currentSpec,\n        }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        // Try to parse JSON error response for better error messages\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) {\n            errorMessage = errorData.message;\n          } else if (errorData.error) {\n            errorMessage = errorData.error;\n          }\n        } catch {\n          // Ignore JSON parsing errors, use default message\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) {\n        throw new Error(\"No response body\");\n      }\n\n      const decoder = new TextDecoder();\n      let buffer = \"\";\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        buffer += decoder.decode(value, { stream: true });\n\n        // Process complete lines\n        const lines = buffer.split(\"\\n\");\n        buffer = lines.pop() ?? \"\";\n\n        for (const line of lines) {\n          const trimmed = line.trim();\n          if (!trimmed) continue;\n          const result = parseLine(trimmed);\n          if (!result) continue;\n          if (result.type === \"usage\") {\n            usage.value = result.usage;\n          } else {\n            rawLines.value = [...rawLines.value, trimmed];\n            currentSpec = applyPatch(currentSpec, result.patch);\n            spec.value = { ...currentSpec };\n          }\n        }\n      }\n\n      // Process any remaining buffer\n      if (buffer.trim()) {\n        const trimmed = buffer.trim();\n        const result = parseLine(trimmed);\n        if (result) {\n          if (result.type === \"usage\") {\n            usage.value = result.usage;\n          } else {\n            rawLines.value = [...rawLines.value, trimmed];\n            currentSpec = applyPatch(currentSpec, result.patch);\n            spec.value = { ...currentSpec };\n          }\n        }\n      }\n\n      onCompleteRef.value?.(currentSpec);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") {\n        return;\n      }\n      const resolvedError = err instanceof Error ? err : new Error(String(err));\n      error.value = resolvedError;\n      onErrorRef.value?.(resolvedError);\n    } finally {\n      isStreaming.value = false;\n    }\n  };\n\n  // Cleanup on unmount\n  onUnmounted(() => {\n    abortController?.abort();\n  });\n\n  return {\n    spec,\n    isStreaming,\n    error,\n    usage,\n    rawLines,\n    send,\n    clear,\n  };\n}\n\n/**\n * Convert a flat element list to a Spec.\n * Input elements use key/parentKey to establish identity and relationships.\n * Output spec uses the map-based format where key is the map entry key\n * and parent-child relationships are expressed through children arrays.\n */\nexport function flatToTree(elements: FlatElement[]): Spec {\n  const elementMap: Record<string, UIElement> = {};\n  let root = \"\";\n\n  // First pass: add all elements to map\n  for (const element of elements) {\n    elementMap[element.key] = {\n      type: element.type,\n      props: element.props,\n      children: [],\n      visible: element.visible,\n    };\n  }\n\n  // Second pass: build parent-child relationships\n  for (const element of elements) {\n    if (element.parentKey) {\n      const parent = elementMap[element.parentKey];\n      if (parent) {\n        if (!parent.children) {\n          parent.children = [];\n        }\n        parent.children.push(element.key);\n      }\n    } else {\n      root = element.key;\n    }\n  }\n\n  return { root, elements: elementMap };\n}\n\n// =============================================================================\n// useBoundProp — Two-way binding helper for $bindState/$bindItem expressions\n// =============================================================================\n\n/**\n * Composable for two-way bound props. Returns `[value, setValue]` where:\n *\n * - `value` is the already-resolved prop value (passed through from render props)\n * - `setValue` writes back to the bound state path (no-op if not bound)\n *\n * Designed to work with the `bindings` map that the renderer provides when\n * a prop uses `{ $bindState: \"/path\" }` or `{ $bindItem: \"field\" }`.\n *\n * @example\n * ```ts\n * import { useBoundProp } from \"@json-render/vue\";\n *\n * const Input: ComponentFn<AppCatalog, \"Input\"> = ({ props, bindings }) => {\n *   const [value, setValue] = useBoundProp<string>(props.value as string, bindings?.value);\n *   return h(\"input\", { value: value ?? \"\", onInput: (e) => setValue(e.target.value) });\n * };\n * ```\n */\nexport function useBoundProp<T>(\n  propValue: T | undefined,\n  bindingPath: string | undefined,\n): [T | undefined, (value: T) => void] {\n  const { set } = useStateStore();\n  return [\n    propValue,\n    (value: T) => {\n      if (bindingPath) set(bindingPath, value);\n    },\n  ];\n}\n\n// =============================================================================\n// buildSpecFromParts — Derive Spec from AI SDK data parts\n// =============================================================================\n\n/**\n * A single part from the AI SDK's `message.parts` array. This is a minimal\n * structural type so that library helpers do not depend on the AI SDK.\n * Fields are optional because different part types carry different data:\n * - Text parts have `text`\n * - Data parts have `data`\n */\nexport interface DataPart {\n  type: string;\n  text?: string;\n  data?: unknown;\n}\n\n/**\n * Type guard that validates a data part payload looks like a valid\n * SpecDataPart before we cast it.\n */\nfunction isSpecDataPart(data: unknown): data is SpecDataPart {\n  if (typeof data !== \"object\" || data === null) return false;\n  const obj = data as Record<string, unknown>;\n  switch (obj.type) {\n    case \"patch\":\n      return typeof obj.patch === \"object\" && obj.patch !== null;\n    case \"flat\":\n    case \"nested\":\n      return typeof obj.spec === \"object\" && obj.spec !== null;\n    default:\n      return false;\n  }\n}\n\n/**\n * Build a `Spec` by replaying all spec data parts from a message's\n * parts array. Returns `null` if no spec data parts are present.\n *\n * Works with the AI SDK's `UIMessage.parts` array. Picks out parts whose\n * `type` is `SPEC_DATA_PART_TYPE` and processes them based on the payload's\n * `type` discriminator: `\"patch\"`, `\"flat\"`, or `\"nested\"`.\n */\nexport function buildSpecFromParts(parts: DataPart[]): Spec | null {\n  const spec: Spec = { root: \"\", elements: {} };\n  let hasSpec = false;\n\n  for (const part of parts) {\n    if (part.type === SPEC_DATA_PART_TYPE) {\n      if (!isSpecDataPart(part.data)) continue;\n      const payload = part.data;\n      if (payload.type === \"patch\") {\n        hasSpec = true;\n        applySpecPatch(spec, payload.patch);\n      } else if (payload.type === \"flat\") {\n        hasSpec = true;\n        Object.assign(spec, payload.spec);\n      } else if (payload.type === \"nested\") {\n        hasSpec = true;\n        const flat = nestedToFlat(payload.spec);\n        Object.assign(spec, flat);\n      }\n    }\n  }\n\n  return hasSpec ? spec : null;\n}\n\n/**\n * Extract and join all text content from a message's parts array.\n *\n * Filters for parts with `type === \"text\"`, trims each one, and joins them\n * with double newlines so that text from separate agent steps renders as\n * distinct paragraphs in markdown.\n */\nexport function getTextFromParts(parts: DataPart[]): string {\n  return parts\n    .filter(\n      (p): p is DataPart & { text: string } =>\n        p.type === \"text\" && typeof p.text === \"string\",\n    )\n    .map((p) => p.text.trim())\n    .filter(Boolean)\n    .join(\"\\n\\n\");\n}\n\n// =============================================================================\n// useJsonRenderMessage — extract spec + text from message parts\n// =============================================================================\n\n/**\n * Composable that extracts both the json-render spec and text content from a\n * message's parts array. Accepts a plain `DataPart[]` or a `Ref<DataPart[]>`\n * for reactive use in streaming scenarios.\n *\n * Returns `ComputedRef`s that recompute whenever `parts` changes.\n *\n * @example\n * ```ts\n * import { useJsonRenderMessage } from \"@json-render/vue\";\n *\n * const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);\n * ```\n */\nexport function useJsonRenderMessage(parts: DataPart[] | Ref<DataPart[]>): {\n  spec: ComputedRef<Spec | null>;\n  text: ComputedRef<string>;\n  hasSpec: ComputedRef<boolean>;\n} {\n  const partsRef = isRef(parts) ? parts : ref(parts);\n  const spec = computed(() => buildSpecFromParts(partsRef.value));\n  const text = computed(() => getTextFromParts(partsRef.value));\n  const hasSpec = computed(\n    () =>\n      spec.value !== null && Object.keys(spec.value.elements || {}).length > 0,\n  );\n  return { spec, text, hasSpec };\n}\n\n// =============================================================================\n// useChatUI — Chat + GenUI composable\n// =============================================================================\n\n/**\n * A single message in the chat, which may contain text, a rendered UI spec, or both.\n */\nexport interface ChatMessage {\n  /** Unique message ID */\n  id: string;\n  /** Who sent this message */\n  role: \"user\" | \"assistant\";\n  /** Text content (conversational prose) */\n  text: string;\n  /** json-render Spec built from JSONL patches (null if no UI was generated) */\n  spec: Spec | null;\n}\n\n/**\n * Options for useChatUI\n */\nexport interface UseChatUIOptions {\n  /** API endpoint that accepts `{ messages: Array<{ role, content }> }` and returns a text stream */\n  api: string;\n  /** Callback when streaming completes for a message */\n  onComplete?: (message: ChatMessage) => void;\n  /** Callback on error */\n  onError?: (error: Error) => void;\n}\n\n/**\n * Return type for useChatUI\n */\nexport interface UseChatUIReturn {\n  /** All messages in the conversation */\n  messages: Ref<ChatMessage[]>;\n  /** Whether currently streaming an assistant response */\n  isStreaming: Ref<boolean>;\n  /** Error from the last request, if any */\n  error: Ref<Error | null>;\n  /** Send a user message */\n  send: (text: string) => Promise<void>;\n  /** Clear all messages and reset the conversation */\n  clear: () => void;\n}\n\nlet chatMessageIdCounter = 0;\nfunction generateChatId(): string {\n  if (\n    typeof crypto !== \"undefined\" &&\n    typeof crypto.randomUUID === \"function\"\n  ) {\n    return crypto.randomUUID();\n  }\n  chatMessageIdCounter += 1;\n  return `msg-${Date.now()}-${chatMessageIdCounter}`;\n}\n\n/**\n * Composable for chat + GenUI experiences.\n *\n * Manages a multi-turn conversation where each assistant message can contain\n * both conversational text and a json-render UI spec. Sends the full message\n * history to the API endpoint, reads the streamed response, and separates\n * text lines from JSONL patch lines using `createMixedStreamParser`.\n *\n * @example\n * ```ts\n * const { messages, isStreaming, send, clear } = useChatUI({ api: \"/api/chat\" });\n *\n * await send(\"Compare weather in NYC and Tokyo\");\n * ```\n */\nexport function useChatUI({\n  api,\n  onComplete,\n  onError,\n}: UseChatUIOptions): UseChatUIReturn {\n  const messages = ref<ChatMessage[]>([]);\n  const isStreaming = ref(false);\n  const error = ref<Error | null>(null);\n\n  const onCompleteRef = ref(onComplete);\n  const onErrorRef = ref(onError);\n\n  let abortController: AbortController | null = null;\n\n  const clear = () => {\n    messages.value = [];\n    error.value = null;\n  };\n\n  const send = async (text: string): Promise<void> => {\n    if (!text.trim()) return;\n\n    // Abort any existing request\n    abortController?.abort();\n    abortController = new AbortController();\n\n    const userMessage: ChatMessage = {\n      id: generateChatId(),\n      role: \"user\",\n      text: text.trim(),\n      spec: null,\n    };\n\n    const assistantId = generateChatId();\n    const assistantMessage: ChatMessage = {\n      id: assistantId,\n      role: \"assistant\",\n      text: \"\",\n      spec: null,\n    };\n\n    // Append user message and empty assistant placeholder\n    messages.value = [...messages.value, userMessage, assistantMessage];\n    isStreaming.value = true;\n    error.value = null;\n\n    // Build messages array for the API (full conversation history + new message).\n    // Vue refs are always current — no stale closure issue unlike React useRef.\n    const historyForApi = [\n      ...messages.value\n        .filter((m) => m.id !== assistantId)\n        .map((m) => ({ role: m.role, content: m.text })),\n      { role: \"user\" as const, content: text.trim() },\n    ];\n\n    // Mutable state for accumulating the assistant response\n    let accumulatedText = \"\";\n    let currentSpec: Spec = { root: \"\", elements: {} };\n    let hasSpec = false;\n\n    try {\n      const response = await fetch(api, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ messages: historyForApi }),\n        signal: abortController.signal,\n      });\n\n      if (!response.ok) {\n        let errorMessage = `HTTP error: ${response.status}`;\n        try {\n          const errorData = await response.json();\n          if (errorData.message) {\n            errorMessage = errorData.message;\n          } else if (errorData.error) {\n            errorMessage = errorData.error;\n          }\n        } catch {\n          // Ignore JSON parsing errors\n        }\n        throw new Error(errorMessage);\n      }\n\n      const reader = response.body?.getReader();\n      if (!reader) {\n        throw new Error(\"No response body\");\n      }\n\n      const decoder = new TextDecoder();\n\n      // Use createMixedStreamParser to classify lines\n      const parser = createMixedStreamParser({\n        onPatch(patch) {\n          hasSpec = true;\n          applySpecPatch(currentSpec, patch);\n          messages.value = messages.value.map((m) =>\n            m.id === assistantId\n              ? {\n                  ...m,\n                  spec: {\n                    root: currentSpec.root,\n                    elements: { ...currentSpec.elements },\n                    ...(currentSpec.state\n                      ? { state: { ...currentSpec.state } }\n                      : {}),\n                  },\n                }\n              : m,\n          );\n        },\n        onText(line) {\n          accumulatedText += (accumulatedText ? \"\\n\" : \"\") + line;\n          messages.value = messages.value.map((m) =>\n            m.id === assistantId ? { ...m, text: accumulatedText } : m,\n          );\n        },\n      });\n\n      while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n        parser.push(decoder.decode(value, { stream: true }));\n      }\n      parser.flush();\n\n      // Build final message for onComplete callback\n      const finalMessage: ChatMessage = {\n        id: assistantId,\n        role: \"assistant\",\n        text: accumulatedText,\n        spec: hasSpec\n          ? {\n              root: currentSpec.root,\n              elements: { ...currentSpec.elements },\n              ...(currentSpec.state ? { state: { ...currentSpec.state } } : {}),\n            }\n          : null,\n      };\n      onCompleteRef.value?.(finalMessage);\n    } catch (err) {\n      if ((err as Error).name === \"AbortError\") {\n        return;\n      }\n      const resolvedError = err instanceof Error ? err : new Error(String(err));\n      error.value = resolvedError;\n      // Remove empty assistant message on error\n      messages.value = messages.value.filter(\n        (m) => m.id !== assistantId || m.text.length > 0,\n      );\n      onErrorRef.value?.(resolvedError);\n    } finally {\n      isStreaming.value = false;\n    }\n  };\n\n  // Cleanup on unmount\n  onUnmounted(() => {\n    abortController?.abort();\n  });\n\n  return {\n    messages,\n    isStreaming,\n    error,\n    send,\n    clear,\n  };\n}\n"
  },
  {
    "path": "packages/vue/src/index.ts",
    "content": "// Composables & Providers\nexport {\n  StateProvider,\n  useStateStore,\n  useStateValue,\n  useStateBinding,\n  type StateContextValue,\n  type StateProviderProps,\n} from \"./composables/state\";\n\nexport {\n  VisibilityProvider,\n  useVisibility,\n  useIsVisible,\n  type VisibilityContextValue,\n} from \"./composables/visibility\";\n\nexport {\n  ActionProvider,\n  useActions,\n  useAction,\n  ConfirmDialog,\n  type ActionContextValue,\n  type ActionProviderProps,\n  type PendingConfirmation,\n  type ConfirmDialogProps,\n} from \"./composables/actions\";\n\nexport {\n  ValidationProvider,\n  useOptionalValidation,\n  useValidation,\n  useFieldValidation,\n  type ValidationContextValue,\n  type ValidationProviderProps,\n  type FieldValidationState,\n} from \"./composables/validation\";\n\nexport {\n  RepeatScopeProvider,\n  useRepeatScope,\n  type RepeatScopeValue,\n} from \"./composables/repeat-scope\";\n\n// Schema\nexport { schema, type VueSchema, type VueSpec } from \"./schema\";\n\n// Core types (re-exported for convenience)\nexport type { Spec, StateStore, ComputedFunction } from \"@json-render/core\";\nexport { createStateStore } from \"@json-render/core\";\n\n// Catalog-aware types for Vue\nexport type {\n  EventHandle,\n  BaseComponentProps,\n  SetState,\n  StateModel,\n  ComponentContext,\n  ComponentFn,\n  Components,\n  ActionFn,\n  Actions,\n} from \"./catalog-types\";\n\n// Hooks\nexport {\n  useUIStream,\n  useChatUI,\n  useBoundProp,\n  flatToTree,\n  buildSpecFromParts,\n  getTextFromParts,\n  useJsonRenderMessage,\n  type UseUIStreamOptions,\n  type UseUIStreamReturn,\n  type UseChatUIOptions,\n  type UseChatUIReturn,\n  type ChatMessage,\n  type DataPart,\n  type TokenUsage,\n} from \"./hooks\";\n\n// Renderer\nexport {\n  // Registry\n  defineRegistry,\n  type DefineRegistryResult,\n  // createRenderer (higher-level, includes providers)\n  createRenderer,\n  type CreateRendererProps,\n  type ComponentMap,\n  // Low-level\n  Renderer,\n  JSONUIProvider,\n  type ComponentRenderProps,\n  type ComponentRegistry,\n  type RendererProps,\n  type JSONUIProviderProps,\n} from \"./renderer\";\n"
  },
  {
    "path": "packages/vue/src/renderer.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { defineComponent, h, type Component } from \"vue\";\nimport { mount } from \"@vue/test-utils\";\nimport type { Spec } from \"@json-render/core\";\nimport { StateProvider } from \"./composables/state\";\nimport { VisibilityProvider } from \"./composables/visibility\";\nimport { ActionProvider } from \"./composables/actions\";\nimport { ValidationProvider } from \"./composables/validation\";\nimport { Renderer, defineRegistry, type ComponentRegistry } from \"./renderer\";\n\n// ---------------------------------------------------------------------------\n// Minimal test catalog and registry\n// ---------------------------------------------------------------------------\n\n// defineRegistry ignores the catalog object at runtime — use any cast\nconst catalog = {} as any;\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) => {\n      const p = props as Record<string, unknown>;\n      return h(\n        \"div\",\n        { \"data-type\": \"card\", \"data-title\": String(p[\"title\"] ?? \"\") },\n        [String(p[\"title\"] ?? \"\"), children],\n      );\n    },\n    Button: ({ props, emit }) => {\n      const p = props as Record<string, unknown>;\n      return h(\n        \"button\",\n        { \"data-type\": \"button\", onClick: () => emit(\"press\") },\n        String(p[\"label\"] ?? \"\"),\n      );\n    },\n  },\n});\n\n// ---------------------------------------------------------------------------\n// Mount helper: wraps in the full provider chain required by ElementRenderer\n// ---------------------------------------------------------------------------\n\nfunction mountRenderer(\n  spec: Spec | null,\n  reg: ComponentRegistry = registry,\n  extraProps: Record<string, unknown> = {},\n  handlers: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<void>\n  > = {},\n  initialState: Record<string, unknown> = {},\n) {\n  return mount(StateProvider as Component, {\n    props: { initialState } as any,\n    slots: {\n      default: () =>\n        h(VisibilityProvider as Component, null, {\n          default: () =>\n            h(ValidationProvider as Component, null, {\n              default: () =>\n                h(ActionProvider as Component, { handlers } as any, {\n                  default: () =>\n                    h(Renderer, { spec, registry: reg, ...extraProps }),\n                }),\n            }),\n        }),\n    },\n  });\n}\n\n// ---------------------------------------------------------------------------\n// defineRegistry tests\n// ---------------------------------------------------------------------------\n\ndescribe(\"defineRegistry\", () => {\n  it(\"wraps a render function and returns a Vue component\", () => {\n    const result = defineRegistry(catalog, {\n      components: {\n        MyComp: () => h(\"span\", null, \"hello\"),\n      },\n    });\n    expect(result.registry).toBeDefined();\n    expect(result.registry[\"MyComp\"]).toBeDefined();\n  });\n\n  it(\"component receives resolved props from the spec element\", () => {\n    const spec: Spec = {\n      root: \"btn1\",\n      elements: {\n        btn1: { type: \"Button\", props: { label: \"Press me\" } },\n      },\n    };\n    const wrapper = mountRenderer(spec);\n    expect(wrapper.find(\"[data-type='button']\").text()).toBe(\"Press me\");\n  });\n\n  it(\"children is passed for container components\", () => {\n    const spec: Spec = {\n      root: \"card1\",\n      elements: {\n        card1: {\n          type: \"Card\",\n          props: { title: \"My Card\" },\n          children: [\"btn1\"],\n        },\n        btn1: { type: \"Button\", props: { label: \"Click\" } },\n      },\n    };\n    const wrapper = mountRenderer(spec);\n    expect(wrapper.find(\"[data-type='card']\").exists()).toBe(true);\n    expect(wrapper.find(\"[data-type='button']\").exists()).toBe(true);\n  });\n\n  it(\"emit('press') fires the corresponding on.press action\", async () => {\n    const handler = vi.fn().mockResolvedValue(undefined);\n    const spec: Spec = {\n      root: \"btn1\",\n      elements: {\n        btn1: {\n          type: \"Button\",\n          props: { label: \"Click\" },\n          on: { press: { action: \"myAction\" } },\n        },\n      },\n    };\n    const wrapper = mountRenderer(spec, registry, {}, { myAction: handler });\n    await wrapper.find(\"[data-type='button']\").trigger(\"click\");\n    expect(handler).toHaveBeenCalledOnce();\n  });\n});\n\n// ---------------------------------------------------------------------------\n// Renderer tests\n// ---------------------------------------------------------------------------\n\ndescribe(\"Renderer\", () => {\n  it(\"renders a single-element spec\", () => {\n    const spec: Spec = {\n      root: \"btn1\",\n      elements: {\n        btn1: { type: \"Button\", props: { label: \"Go\" } },\n      },\n    };\n    const wrapper = mountRenderer(spec);\n    expect(wrapper.find(\"[data-type='button']\").exists()).toBe(true);\n    expect(wrapper.find(\"[data-type='button']\").text()).toBe(\"Go\");\n  });\n\n  it(\"renders a nested spec (parent contains a child by key reference)\", () => {\n    const spec: Spec = {\n      root: \"card1\",\n      elements: {\n        card1: {\n          type: \"Card\",\n          props: { title: \"Root\" },\n          children: [\"btn1\"],\n        },\n        btn1: { type: \"Button\", props: { label: \"Action\" } },\n      },\n    };\n    const wrapper = mountRenderer(spec);\n    const card = wrapper.find(\"[data-type='card']\");\n    expect(card.exists()).toBe(true);\n    expect(card.find(\"[data-type='button']\").exists()).toBe(true);\n  });\n\n  it(\"uses fallback component for unknown element types\", () => {\n    const fallback = defineComponent({\n      setup() {\n        return () => h(\"span\", { \"data-type\": \"fallback\" }, \"fallback\");\n      },\n    });\n    const spec: Spec = {\n      root: \"el1\",\n      elements: {\n        el1: { type: \"Unknown\", props: {} },\n      },\n    };\n    const wrapper = mountRenderer(spec, registry, { fallback });\n    expect(wrapper.find(\"[data-type='fallback']\").exists()).toBe(true);\n  });\n\n  it(\"passes loading prop through to registered components\", () => {\n    let receivedLoading: boolean | undefined;\n    const { registry: testRegistry } = defineRegistry(catalog, {\n      components: {\n        Widget: ({ loading }) => {\n          receivedLoading = loading;\n          return h(\"div\", null, \"widget\");\n        },\n      },\n    });\n    const spec: Spec = {\n      root: \"w1\",\n      elements: { w1: { type: \"Widget\", props: {} } },\n    };\n    mountRenderer(spec, testRegistry, { loading: true });\n    expect(receivedLoading).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/vue/src/renderer.ts",
    "content": "import {\n  computed,\n  defineComponent,\n  h,\n  inject,\n  onErrorCaptured,\n  provide,\n  ref,\n  watch,\n  type Component,\n  type ComputedRef,\n  type PropType,\n  type VNode,\n} from \"vue\";\nimport type {\n  UIElement,\n  Spec,\n  ActionBinding,\n  Catalog,\n  ComputedFunction,\n  SchemaDefinition,\n  StateStore,\n} from \"@json-render/core\";\nimport {\n  resolveElementProps,\n  resolveBindings,\n  resolveActionParam,\n  evaluateVisibility,\n  getByPath,\n  type PropResolutionContext,\n} from \"@json-render/core\";\nimport type {\n  Components,\n  Actions,\n  ActionFn,\n  SetState,\n  StateModel,\n  CatalogHasActions,\n  EventHandle,\n} from \"./catalog-types\";\nimport { useVisibility } from \"./composables/visibility\";\nimport { useActions } from \"./composables/actions\";\nimport { useStateStore } from \"./composables/state\";\nimport { StateProvider } from \"./composables/state\";\nimport { VisibilityProvider } from \"./composables/visibility\";\nimport { ActionProvider } from \"./composables/actions\";\nimport { ValidationProvider } from \"./composables/validation\";\nimport { ConfirmDialog } from \"./composables/actions\";\nimport {\n  RepeatScopeProvider,\n  useRepeatScope,\n} from \"./composables/repeat-scope\";\n\n/**\n * Props passed to component renderers\n */\nexport interface ComponentRenderProps<P = Record<string, unknown>> {\n  /** The element being rendered */\n  element: UIElement<string, P>;\n  /** Emit a named event */\n  emit: (event: string) => void;\n  /** Get an event handle with metadata */\n  on: (event: string) => EventHandle;\n  /**\n   * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.\n   * Maps prop name → absolute state path for write-back.\n   */\n  bindings?: Record<string, string>;\n  /** Whether the parent is loading */\n  loading?: boolean;\n}\n\n/**\n * Registry of component renderers (Vue component definitions)\n */\nexport type ComponentRegistry = Record<string, Component>;\n\n/**\n * Props for the Renderer component\n */\nexport interface RendererProps {\n  spec: Spec | null;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: Component;\n}\n\n// ---------------------------------------------------------------------------\n// FunctionsContext — provides $computed functions to the element tree\n// ---------------------------------------------------------------------------\n\nconst EMPTY_FUNCTIONS: Record<string, ComputedFunction> = {};\nconst FUNCTIONS_KEY = Symbol(\"json-render:functions\");\n\nconst FunctionsProvider = defineComponent({\n  name: \"FunctionsProvider\",\n  props: {\n    functions: {\n      type: Object as PropType<Record<string, ComputedFunction>>,\n      default: undefined,\n    },\n  },\n  setup(props, { slots }) {\n    const fns = computed(() => props.functions ?? EMPTY_FUNCTIONS);\n    provide(FUNCTIONS_KEY, fns);\n    return () => slots.default?.();\n  },\n});\n\nfunction useFunctions(): ComputedRef<Record<string, ComputedFunction>> {\n  return inject<ComputedRef<Record<string, ComputedFunction>>>(\n    FUNCTIONS_KEY,\n    computed(() => EMPTY_FUNCTIONS),\n  );\n}\n\n// ---------------------------------------------------------------------------\n// ElementErrorBoundary — catches rendering errors in individual elements\n// ---------------------------------------------------------------------------\n\nconst ElementErrorBoundary = defineComponent({\n  name: \"ElementErrorBoundary\",\n  props: {\n    elementType: {\n      type: String,\n      required: true,\n    },\n  },\n  setup(props, { slots }) {\n    const hasError = ref(false);\n\n    onErrorCaptured((error) => {\n      console.error(\n        `[json-render] Rendering error in <${props.elementType}>:`,\n        error,\n      );\n      hasError.value = true;\n      return false; // prevent propagation\n    });\n\n    return () => {\n      if (hasError.value) return null;\n      return slots.default?.();\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// resolveAndExecuteBindings — shared helper for emitEvent / watch handlers\n// ---------------------------------------------------------------------------\n\nasync function resolveAndExecuteBindings(\n  actionBindings: ActionBinding[],\n  ctx: PropResolutionContext,\n  getSnapshot: () => Record<string, unknown>,\n  execute: (binding: ActionBinding) => Promise<void>,\n  cancelled?: () => boolean,\n): Promise<void> {\n  for (const b of actionBindings) {\n    if (cancelled?.()) break;\n    if (!b.params) {\n      await execute(b);\n      if (cancelled?.()) break;\n      continue;\n    }\n    const liveCtx: PropResolutionContext = {\n      ...ctx,\n      stateModel: getSnapshot(),\n    };\n    const resolved: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(b.params)) {\n      resolved[key] = resolveActionParam(val, liveCtx);\n    }\n    await execute({ ...b, params: resolved });\n    if (cancelled?.()) break;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// ElementRenderer — renders a single element from the spec\n// ---------------------------------------------------------------------------\n\ninterface ElementRendererInternalProps {\n  element: UIElement;\n  spec: Spec;\n  registry: ComponentRegistry;\n  loading?: boolean;\n  fallback?: Component;\n}\n\nconst ElementRenderer = defineComponent({\n  name: \"JsonRenderElement\",\n  props: {\n    element: {\n      type: Object as PropType<UIElement>,\n      required: true,\n    },\n    spec: {\n      type: Object as PropType<Spec>,\n      required: true,\n    },\n    registry: {\n      type: Object as PropType<ComponentRegistry>,\n      required: true,\n    },\n    loading: {\n      type: Boolean,\n      default: undefined,\n    },\n    fallback: {\n      type: Object as PropType<Component>,\n      default: undefined,\n    },\n  },\n  setup(props: ElementRendererInternalProps) {\n    const repeatScope = useRepeatScope();\n    const { ctx: visibilityCtx } = useVisibility();\n    const { execute } = useActions();\n    const { getSnapshot, state: watchState } = useStateStore();\n    const functions = useFunctions();\n\n    // Build context with repeat scope and $computed functions\n    const fullCtx = computed<PropResolutionContext>(() => {\n      const base: PropResolutionContext = repeatScope\n        ? {\n            ...visibilityCtx.value,\n            repeatItem: repeatScope.item,\n            repeatIndex: repeatScope.index,\n            repeatBasePath: repeatScope.basePath,\n          }\n        : { ...visibilityCtx.value };\n      base.functions = functions.value;\n      return base;\n    });\n\n    // Create emit function\n    const emitEvent = async (eventName: string): Promise<void> => {\n      const binding = props.element.on?.[eventName];\n      if (!binding) return;\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      await resolveAndExecuteBindings(\n        actionBindings,\n        fullCtx.value,\n        getSnapshot,\n        execute,\n      );\n    };\n\n    // Create on() function\n    const onEvent = (eventName: string): EventHandle => {\n      const binding = props.element.on?.[eventName];\n      if (!binding) {\n        return { emit: () => {}, shouldPreventDefault: false, bound: false };\n      }\n      const actionBindings = Array.isArray(binding) ? binding : [binding];\n      const shouldPreventDefault = actionBindings.some((b) => b.preventDefault);\n      return {\n        emit: () => {\n          void emitEvent(eventName);\n        },\n        shouldPreventDefault,\n        bound: true,\n      };\n    };\n\n    // Watch effect: fire actions when watched state paths change.\n    const watchedValues = computed(() => {\n      const cfg = props.element.watch;\n      if (!cfg) return undefined;\n      const values: Record<string, unknown> = {};\n      for (const path of Object.keys(cfg)) {\n        values[path] = getByPath(watchState.value, path);\n      }\n      return values;\n    });\n\n    watch(\n      watchedValues,\n      (current, prev, onCleanup) => {\n        const cfg = props.element.watch;\n        if (!cfg || !current) return;\n\n        let cancelled = false;\n        onCleanup(() => {\n          cancelled = true;\n        });\n\n        const paths = Object.keys(cfg);\n        void (async () => {\n          for (const path of paths) {\n            if (cancelled) break;\n            if (prev && current[path] === prev[path]) continue;\n            const binding = cfg[path];\n            if (!binding) continue;\n            const bindings = Array.isArray(binding) ? binding : [binding];\n            await resolveAndExecuteBindings(\n              bindings,\n              fullCtx.value,\n              getSnapshot,\n              execute,\n              () => cancelled,\n            );\n          }\n        })().catch(console.error);\n      },\n      { deep: true },\n    );\n\n    return () => {\n      const ctx = fullCtx.value;\n\n      // Evaluate visibility\n      const isVisible =\n        props.element.visible === undefined\n          ? true\n          : evaluateVisibility(props.element.visible, ctx);\n\n      if (!isVisible) return null;\n\n      // Resolve bindings and props\n      const rawProps = props.element.props as Record<string, unknown>;\n      const elementBindings = resolveBindings(rawProps, ctx);\n      const resolvedProps = resolveElementProps(rawProps, ctx);\n\n      const resolvedElement =\n        resolvedProps !== props.element.props\n          ? { ...props.element, props: resolvedProps }\n          : props.element;\n\n      // Get component from registry\n      const Component = props.registry[resolvedElement.type] ?? props.fallback;\n\n      if (!Component) {\n        console.warn(\n          `[json-render] No renderer for component type: ${resolvedElement.type}`,\n        );\n        return null;\n      }\n\n      // Render children\n      const childrenVNodes: VNode | VNode[] | undefined = resolvedElement.repeat\n        ? h(RepeatChildren, {\n            element: resolvedElement,\n            spec: props.spec,\n            registry: props.registry,\n            loading: props.loading,\n            fallback: props.fallback,\n          })\n        : (resolvedElement.children\n            ?.map((childKey) => {\n              const childElement = props.spec.elements[childKey];\n              if (!childElement) {\n                if (!props.loading) {\n                  console.warn(\n                    `[json-render] Missing element \"${childKey}\" referenced as child of \"${resolvedElement.type}\". This element will not render.`,\n                  );\n                }\n                return null;\n              }\n              return h(ElementRenderer, {\n                key: childKey,\n                element: childElement,\n                spec: props.spec,\n                registry: props.registry,\n                loading: props.loading,\n                fallback: props.fallback,\n              });\n            })\n            .filter((n): n is VNode => n !== null) ?? undefined);\n\n      return h(\n        ElementErrorBoundary,\n        { elementType: resolvedElement.type },\n        {\n          default: () =>\n            h(\n              Component,\n              {\n                element: resolvedElement,\n                emit: emitEvent,\n                on: onEvent,\n                bindings: elementBindings,\n                loading: props.loading,\n              },\n              { default: () => childrenVNodes },\n            ),\n        },\n      );\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// RepeatChildren — renders child elements once per item in a state array\n// ---------------------------------------------------------------------------\n\nconst RepeatChildren = defineComponent({\n  name: \"JsonRenderRepeatChildren\",\n  props: {\n    element: {\n      type: Object as PropType<UIElement>,\n      required: true,\n    },\n    spec: {\n      type: Object as PropType<Spec>,\n      required: true,\n    },\n    registry: {\n      type: Object as PropType<ComponentRegistry>,\n      required: true,\n    },\n    loading: {\n      type: Boolean,\n      default: undefined,\n    },\n    fallback: {\n      type: Object as PropType<Component>,\n      default: undefined,\n    },\n  },\n  setup(props) {\n    const { state } = useStateStore();\n\n    return () => {\n      const repeat = props.element.repeat;\n      if (!repeat?.statePath) return null;\n      const statePath = repeat.statePath;\n      const raw = getByPath(state.value, statePath);\n      const items = Array.isArray(raw) ? (raw as unknown[]) : [];\n\n      return items.map((itemValue, index) => {\n        const key =\n          repeat.key && typeof itemValue === \"object\" && itemValue !== null\n            ? String(\n                (itemValue as Record<string, unknown>)[repeat.key] ?? index,\n              )\n            : String(index);\n\n        return h(\n          RepeatScopeProvider,\n          { key, item: itemValue, index, basePath: `${statePath}/${index}` },\n          {\n            default: () =>\n              props.element.children\n                ?.map((childKey) => {\n                  const childElement = props.spec.elements[childKey];\n                  if (!childElement) {\n                    if (!props.loading) {\n                      console.warn(\n                        `[json-render] Missing element \"${childKey}\" referenced as child of \"${props.element.type}\" (repeat). This element will not render.`,\n                      );\n                    }\n                    return null;\n                  }\n                  return h(ElementRenderer, {\n                    key: childKey,\n                    element: childElement,\n                    spec: props.spec,\n                    registry: props.registry,\n                    loading: props.loading,\n                    fallback: props.fallback,\n                  });\n                })\n                .filter((n): n is VNode => n !== null) ?? null,\n          },\n        );\n      });\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// Renderer — main exported component\n// ---------------------------------------------------------------------------\n\n/**\n * Main renderer component\n */\nexport const Renderer = defineComponent({\n  name: \"JsonRenderer\",\n  props: {\n    spec: {\n      type: Object as PropType<Spec | null>,\n      default: null,\n    },\n    registry: {\n      type: Object as PropType<ComponentRegistry>,\n      required: true,\n    },\n    loading: {\n      type: Boolean,\n      default: undefined,\n    },\n    fallback: {\n      type: Object as PropType<Component>,\n      default: undefined,\n    },\n  },\n  setup(props) {\n    return () => {\n      if (!props.spec?.root) return null;\n\n      const rootElement = props.spec.elements[props.spec.root];\n      if (!rootElement) return null;\n\n      return h(ElementRenderer, {\n        element: rootElement,\n        spec: props.spec,\n        registry: props.registry,\n        loading: props.loading,\n        fallback: props.fallback,\n      });\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// ConfirmationDialogManager\n// ---------------------------------------------------------------------------\n\nconst ConfirmationDialogManager = defineComponent({\n  name: \"ConfirmationDialogManager\",\n  setup() {\n    const { pendingConfirmation, confirm, cancel } = useActions();\n\n    return () => {\n      if (!pendingConfirmation?.action.confirm) return null;\n\n      return h(ConfirmDialog, {\n        confirm: pendingConfirmation.action.confirm,\n        onConfirm: confirm,\n        onCancel: cancel,\n      });\n    };\n  },\n});\n\n// ---------------------------------------------------------------------------\n// JSONUIProvider — combined provider for all contexts\n// ---------------------------------------------------------------------------\n\n/**\n * Props for JSONUIProvider\n */\nexport interface JSONUIProviderProps {\n  registry: ComponentRegistry;\n  store?: StateStore;\n  initialState?: Record<string, unknown>;\n  handlers?: Record<\n    string,\n    (params: Record<string, unknown>) => Promise<unknown> | unknown\n  >;\n  navigate?: (path: string) => void;\n  validationFunctions?: Record<\n    string,\n    (value: unknown, args?: Record<string, unknown>) => boolean\n  >;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n}\n\n/**\n * Combined provider for all JSONUI contexts\n */\nexport const JSONUIProvider = defineComponent({\n  name: \"JSONUIProvider\",\n  props: {\n    registry: {\n      type: Object as PropType<ComponentRegistry>,\n      required: true,\n    },\n    store: {\n      type: Object as PropType<StateStore>,\n      default: undefined,\n    },\n    initialState: {\n      type: Object as PropType<Record<string, unknown>>,\n      default: undefined,\n    },\n    handlers: {\n      type: Object as PropType<\n        Record<\n          string,\n          (params: Record<string, unknown>) => Promise<unknown> | unknown\n        >\n      >,\n      default: undefined,\n    },\n    navigate: {\n      type: Function as PropType<(path: string) => void>,\n      default: undefined,\n    },\n    validationFunctions: {\n      type: Object as PropType<\n        Record<\n          string,\n          (value: unknown, args?: Record<string, unknown>) => boolean\n        >\n      >,\n      default: undefined,\n    },\n    functions: {\n      type: Object as PropType<Record<string, ComputedFunction>>,\n      default: undefined,\n    },\n    onStateChange: {\n      type: Function as PropType<\n        (changes: Array<{ path: string; value: unknown }>) => void\n      >,\n      default: undefined,\n    },\n  },\n  setup(props, { slots }) {\n    return () =>\n      h(\n        StateProvider,\n        {\n          store: props.store,\n          initialState: props.initialState,\n          onStateChange: props.onStateChange,\n        },\n        {\n          default: () =>\n            h(VisibilityProvider, null, {\n              default: () =>\n                h(\n                  ValidationProvider,\n                  { customFunctions: props.validationFunctions },\n                  {\n                    default: () =>\n                      h(\n                        ActionProvider,\n                        { handlers: props.handlers, navigate: props.navigate },\n                        {\n                          default: () =>\n                            h(\n                              FunctionsProvider,\n                              { functions: props.functions },\n                              {\n                                default: () => [\n                                  slots.default?.(),\n                                  h(ConfirmationDialogManager),\n                                ],\n                              },\n                            ),\n                        },\n                      ),\n                  },\n                ),\n            }),\n        },\n      );\n  },\n});\n\n// ============================================================================\n// defineRegistry\n// ============================================================================\n\n/**\n * Result returned by defineRegistry\n */\nexport interface DefineRegistryResult {\n  registry: ComponentRegistry;\n  handlers: (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;\n  executeAction: (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state?: StateModel,\n  ) => Promise<void>;\n}\n\ntype DefineRegistryOptions<C extends Catalog> = {\n  components?: Components<C>;\n} & (CatalogHasActions<C> extends true\n  ? { actions: Actions<C> }\n  : { actions?: Actions<C> });\n\ntype DefineRegistryComponentFn = (ctx: {\n  props: unknown;\n  children?: VNode | VNode[];\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}) => VNode | VNode[] | null | string;\n\ntype DefineRegistryActionFn = (\n  params: Record<string, unknown> | undefined,\n  setState: SetState,\n  state: StateModel,\n) => Promise<void>;\n\n/**\n * Create a registry from a catalog with components and/or actions.\n *\n * @example\n * ```ts\n * // Components only\n * const { registry } = defineRegistry(catalog, {\n *   components: {\n *     Card: ({ props, children }) => h('div', { class: 'card' }, [props.title, children]),\n *   },\n * });\n *\n * // Both\n * const { registry, handlers, executeAction } = defineRegistry(catalog, {\n *   components: { ... },\n *   actions: { ... },\n * });\n * ```\n */\nexport function defineRegistry<C extends Catalog>(\n  _catalog: C,\n  options: DefineRegistryOptions<C>,\n): DefineRegistryResult {\n  const registry: ComponentRegistry = {};\n\n  if (options.components) {\n    for (const [name, componentFn] of Object.entries(options.components)) {\n      registry[name] = defineComponent({\n        name: `JsonRenderRegistry_${name}`,\n        props: {\n          element: {\n            type: Object as PropType<UIElement>,\n            required: true,\n          },\n          emit: {\n            type: Function as PropType<(event: string) => void>,\n            required: true,\n          },\n          on: {\n            type: Function as PropType<(event: string) => EventHandle>,\n            required: true,\n          },\n          bindings: {\n            type: Object as PropType<Record<string, string>>,\n            default: undefined,\n          },\n          loading: {\n            type: Boolean,\n            default: undefined,\n          },\n        },\n        setup(registryProps, { slots }) {\n          return () =>\n            (componentFn as DefineRegistryComponentFn)({\n              props: registryProps.element.props,\n              children: slots.default?.(),\n              emit: registryProps.emit,\n              on: registryProps.on,\n              bindings: registryProps.bindings,\n              loading: registryProps.loading,\n            });\n        },\n      });\n    }\n  }\n\n  const actionMap = options.actions\n    ? (Object.entries(options.actions) as Array<\n        [string, DefineRegistryActionFn]\n      >)\n    : [];\n\n  const handlers = (\n    getSetState: () => SetState | undefined,\n    getState: () => StateModel,\n  ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {\n    const result: Record<\n      string,\n      (params: Record<string, unknown>) => Promise<void>\n    > = {};\n    for (const [name, actionFn] of actionMap) {\n      result[name] = async (params) => {\n        const setState = getSetState();\n        const state = getState();\n        if (setState) {\n          await actionFn(params, setState, state);\n        }\n      };\n    }\n    return result;\n  };\n\n  const executeAction = async (\n    actionName: string,\n    params: Record<string, unknown> | undefined,\n    setState: SetState,\n    state: StateModel = {},\n  ): Promise<void> => {\n    const entry = actionMap.find(([name]) => name === actionName);\n    if (entry) {\n      await entry[1](params, setState, state);\n    } else {\n      console.warn(`Unknown action: ${actionName}`);\n    }\n  };\n\n  return { registry, handlers, executeAction };\n}\n\n// ============================================================================\n// createRenderer\n// ============================================================================\n\n/**\n * Props for renderers created with createRenderer\n */\nexport interface CreateRendererProps {\n  spec: Spec | null;\n  store?: StateStore;\n  state?: Record<string, unknown>;\n  onAction?: (actionName: string, params?: Record<string, unknown>) => void;\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;\n  /** Named functions for `$computed` expressions in props */\n  functions?: Record<string, ComputedFunction>;\n  loading?: boolean;\n  fallback?: Component;\n}\n\n/**\n * Component map type — maps component names to Vue components\n */\nexport type ComponentMap<\n  TComponents extends Record<string, { props: unknown }>,\n> = {\n  [K in keyof TComponents]: Component;\n};\n\n/**\n * Create a renderer from a catalog\n *\n * @example\n * ```typescript\n * const DashboardRenderer = createRenderer(dashboardCatalog, {\n *   Card: ({ props, children }) => h('div', { class: 'card' }, children),\n *   Metric: ({ props }) => h('span', null, props.value),\n * });\n *\n * // Usage in template\n * <DashboardRenderer :spec=\"aiGeneratedSpec\" :state=\"state\" />\n * ```\n */\nexport function createRenderer<\n  TDef extends SchemaDefinition,\n  TCatalog extends { components: Record<string, { props: unknown }> },\n>(\n  catalog: Catalog<TDef, TCatalog>,\n  components: ComponentMap<TCatalog[\"components\"]>,\n): Component {\n  const registry: ComponentRegistry =\n    components as unknown as ComponentRegistry;\n\n  return defineComponent({\n    name: \"CatalogRenderer\",\n    props: {\n      spec: {\n        type: Object as PropType<Spec | null>,\n        default: null,\n      },\n      store: {\n        type: Object as PropType<StateStore>,\n        default: undefined,\n      },\n      state: {\n        type: Object as PropType<Record<string, unknown>>,\n        default: undefined,\n      },\n      onAction: {\n        type: Function as PropType<\n          (actionName: string, params?: Record<string, unknown>) => void\n        >,\n        default: undefined,\n      },\n      onStateChange: {\n        type: Function as PropType<\n          (changes: Array<{ path: string; value: unknown }>) => void\n        >,\n        default: undefined,\n      },\n      functions: {\n        type: Object as PropType<Record<string, ComputedFunction>>,\n        default: undefined,\n      },\n      loading: {\n        type: Boolean,\n        default: undefined,\n      },\n      fallback: {\n        type: Object as PropType<Component>,\n        default: undefined,\n      },\n    },\n    setup(rendererProps) {\n      return () => {\n        // Build the action handlers proxy if onAction is provided\n        const actionHandlers = rendererProps.onAction\n          ? new Proxy(\n              {} as Record<\n                string,\n                (params: Record<string, unknown>) => void | Promise<void>\n              >,\n              {\n                get: (_target, prop: string) => {\n                  return (params: Record<string, unknown>) =>\n                    rendererProps.onAction!(prop, params);\n                },\n                has: () => true,\n              },\n            )\n          : undefined;\n\n        return h(\n          StateProvider,\n          {\n            store: rendererProps.store,\n            initialState: rendererProps.state,\n            onStateChange: rendererProps.onStateChange,\n          },\n          {\n            default: () =>\n              h(VisibilityProvider, null, {\n                default: () =>\n                  h(ValidationProvider, null, {\n                    default: () =>\n                      h(\n                        ActionProvider,\n                        { handlers: actionHandlers },\n                        {\n                          default: () =>\n                            h(\n                              FunctionsProvider,\n                              { functions: rendererProps.functions },\n                              {\n                                default: () => [\n                                  h(Renderer, {\n                                    spec: rendererProps.spec,\n                                    registry,\n                                    loading: rendererProps.loading,\n                                    fallback: rendererProps.fallback,\n                                  }),\n                                  h(ConfirmationDialogManager),\n                                ],\n                              },\n                            ),\n                        },\n                      ),\n                  }),\n              }),\n          },\n        );\n      };\n    },\n  });\n}\n"
  },
  {
    "path": "packages/vue/src/schema.ts",
    "content": "import { defineSchema } from \"@json-render/core\";\n\n/**\n * The schema for @json-render/vue\n *\n * Defines:\n * - Spec: A flat tree of elements with keys, types, props, and children references\n * - Catalog: Components with props schemas, and optional actions\n */\nexport const schema = defineSchema(\n  (s) => ({\n    // What the AI-generated SPEC looks like\n    spec: s.object({\n      /** Root element key */\n      root: s.string(),\n      /** Flat map of elements by key */\n      elements: s.record(\n        s.object({\n          /** Component type from catalog */\n          type: s.ref(\"catalog.components\"),\n          /** Component props */\n          props: s.propsOf(\"catalog.components\"),\n          /** Child element keys (flat reference) */\n          children: s.array(s.string()),\n          /** Visibility condition */\n          visible: s.any(),\n        }),\n      ),\n    }),\n\n    // What the CATALOG must provide\n    catalog: s.object({\n      /** Component definitions */\n      components: s.map({\n        /** Zod schema for component props */\n        props: s.zod(),\n        /** Slots for this component. Use ['default'] for children, or named slots like ['header', 'footer'] */\n        slots: s.array(s.string()),\n        /** Description for AI generation hints */\n        description: s.string(),\n        /** Example prop values used in prompt examples (auto-generated from Zod schema if omitted) */\n        example: s.any(),\n      }),\n      /** Action definitions (optional) */\n      actions: s.map({\n        /** Zod schema for action params */\n        params: s.zod(),\n        /** Description for AI generation hints */\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    builtInActions: [\n      {\n        name: \"setState\",\n        description:\n          \"Update a value in the state model at the given statePath. Params: { statePath: string, value: any }\",\n      },\n      {\n        name: \"pushState\",\n        description:\n          'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {\"$state\":\"/path\"} refs and \"$id\" for auto IDs.',\n      },\n      {\n        name: \"removeState\",\n        description:\n          \"Remove an item from an array in state by index. Params: { statePath: string, index: number }\",\n      },\n      {\n        name: \"validateForm\",\n        description:\n          \"Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.\",\n      },\n    ],\n    defaultRules: [\n      // Element integrity\n      \"CRITICAL INTEGRITY CHECK: Before outputting ANY element that references children, you MUST have already output (or will output) each child as its own element. If an element has children: ['a', 'b'], then elements 'a' and 'b' MUST exist. A missing child element causes that entire branch of the UI to be invisible.\",\n      \"SELF-CHECK: After generating all elements, mentally walk the tree from root. Every key in every children array must resolve to a defined element. If you find a gap, output the missing element immediately.\",\n\n      // Field placement\n      'CRITICAL: The \"visible\" field goes on the ELEMENT object, NOT inside \"props\". Correct: {\"type\":\"<ComponentName>\",\"props\":{},\"visible\":{\"$state\":\"/tab\",\"eq\":\"home\"},\"children\":[...]}.',\n      'CRITICAL: The \"on\" field goes on the ELEMENT object, NOT inside \"props\". Use on.press, on.change, on.submit etc. NEVER put action/actionParams inside props.',\n\n      // State and data\n      \"When the user asks for a UI that displays data (e.g. blog posts, products, users), ALWAYS include a state field with realistic sample data. The state field is a top-level field on the spec (sibling of root/elements).\",\n      'When building repeating content backed by a state array (e.g. posts, products, items), use the \"repeat\" field on a container element. Example: { \"type\": \"<ContainerComponent>\", \"props\": {}, \"repeat\": { \"statePath\": \"/posts\", \"key\": \"id\" }, \"children\": [\"post-card\"] }. Replace <ContainerComponent> with an appropriate component from the AVAILABLE COMPONENTS list. Inside repeated children, use { \"$item\": \"field\" } to read a field from the current item, and { \"$index\": true } for the current array index. For two-way binding to an item field use { \"$bindItem\": \"completed\" }. Do NOT hardcode individual elements for each array item.',\n\n      // Design quality\n      \"Design with visual hierarchy: use container components to group content, heading components for section titles, proper spacing, and status indicators. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"For data-rich UIs, use multi-column layout components if available. For forms and single-column content, use vertical layout components. ONLY use components from the AVAILABLE COMPONENTS list.\",\n      \"Always include realistic, professional-looking sample data. For blogs include 3-4 posts with varied titles, authors, dates, categories. For products include names, prices, images. Never leave data empty.\",\n    ],\n  },\n);\n\n/**\n * Type for the Vue schema\n */\nexport type VueSchema = typeof schema;\n\n/**\n * Infer the spec type from a catalog\n */\nexport type VueSpec<TCatalog> = typeof schema extends {\n  createCatalog: (catalog: TCatalog) => { _specType: infer S };\n}\n  ? S\n  : never;\n"
  },
  {
    "path": "packages/vue/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/vue/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\", \"src/schema.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"vue\", \"@json-render/core\"],\n});\n"
  },
  {
    "path": "packages/xstate/CHANGELOG.md",
    "content": "# @json-render/xstate\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Minor Changes\n\n- 9cef4e9: Dynamic forms, Vue renderer, XState Store adapter, and computed values.\n\n  ### New: `@json-render/vue` Package\n\n  Vue 3 renderer for json-render. Full feature parity with `@json-render/react` including data binding, visibility conditions, actions, validation, repeat scopes, and streaming.\n  - `defineRegistry` — create type-safe component registries from catalogs\n  - `Renderer` — render specs as Vue component trees\n  - Providers: `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider`\n  - Composables: `useStateStore`, `useStateValue`, `useStateBinding`, `useActions`, `useAction`, `useIsVisible`, `useFieldValidation`\n  - Streaming: `useUIStream`, `useChatUI`\n  - External store support via `StateStore` interface\n\n  ### New: `@json-render/xstate` Package\n\n  XState Store (atom) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend.\n  - `xstateStoreStateStore({ atom })` — creates a `StateStore` from an `@xstate/store` atom\n  - Requires `@xstate/store` v3+\n\n  ### New: `$computed` Expressions\n\n  Call registered functions from prop expressions:\n  - `{ \"$computed\": \"functionName\", \"args\": { \"key\": <expression> } }` — calls a named function with resolved args\n  - Functions registered via catalog and provided at runtime through `functions` prop on `JSONUIProvider` / `createRenderer`\n  - `ComputedFunction` type exported from `@json-render/core`\n\n  ### New: `$template` Expressions\n\n  Interpolate state values into strings:\n  - `{ \"$template\": \"Hello, ${/user/name}!\" }` — replaces `${/path}` references with state values\n  - Missing paths resolve to empty string\n\n  ### New: State Watchers\n\n  React to state changes by triggering actions:\n  - `watch` field on elements maps state paths to action bindings\n  - Fires when watched values change (not on initial render)\n  - Supports cascading dependencies (e.g. country → city loading)\n  - `watch` is a top-level field on elements (sibling of type/props/children), not inside props\n  - Spec validator detects and auto-fixes `watch` placed inside props\n\n  ### New: Cross-Field Validation Functions\n\n  New built-in validation functions for cross-field comparisons:\n  - `equalTo` — alias for `matches` with clearer semantics\n  - `lessThan` — value must be less than another field (numbers, strings, coerced)\n  - `greaterThan` — value must be greater than another field\n  - `requiredIf` — required only when a condition field is truthy\n  - Validation args now resolve through `resolvePropValue` for consistent `$state` expression handling\n\n  ### New: `validateForm` Action (React)\n\n  Built-in action that validates all registered form fields at once:\n  - Runs `validateAll()` synchronously and writes `{ valid, errors }` to state\n  - Default state path: `/formValidation` (configurable via `statePath` param)\n  - Added to React schema's built-in actions list\n\n  ### Improved: shadcn/ui Validation\n\n  All form components now support validation:\n  - Checkbox, Radio, Switch — added `checks` and `validateOn` props\n  - Input, Textarea, Select — added `validateOn` prop (controls timing: change/blur/submit)\n  - Shared validation schemas reduce catalog definition duplication\n\n  ### Improved: React Provider Tree\n\n  Reordered provider nesting so `ValidationProvider` wraps `ActionProvider`, enabling `validateForm` to access validation state. Added `useOptionalValidation` hook for non-throwing access.\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n"
  },
  {
    "path": "packages/xstate/README.md",
    "content": "# @json-render/xstate\n\n[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/xstate @json-render/core @json-render/react @xstate/store\n```\n\n> [!NOTE]\n> This adapter requires `@xstate/store` v3+.\n\n## Usage\n\n```ts\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom\nconst uiAtom = createAtom({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = xstateStoreStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through @xstate/store */}\n</StateProvider>\n```\n\n## API\n\n### `xstateStoreStateStore(options)`\n\nCreates a `StateStore` backed by an `@xstate/store` atom.\n\n#### Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `Atom<StateModel>` | Yes | An `@xstate/store` atom (from `createAtom`) holding the json-render state model |\n"
  },
  {
    "path": "packages/xstate/package.json",
    "content": "{\n  \"name\": \"@json-render/xstate\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"XState Store adapter for json-render StateStore\",\n  \"keywords\": [\n    \"json-render\",\n    \"xstate\",\n    \"xstate-store\",\n    \"state-management\",\n    \"adapter\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/xstate\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"peerDependencies\": {\n    \"@xstate/store\": \">=3.0.0\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"@xstate/store\": \"^3.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/xstate/src/index.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"./index\";\n\nfunction createTestStore(initial: Record<string, unknown> = {}) {\n  const atom = createAtom<Record<string, unknown>>(initial);\n  const store = xstateStoreStateStore({ atom });\n  return { atom, store };\n}\n\ndescribe(\"xstateStoreStateStore\", () => {\n  it(\"get/set round-trip\", () => {\n    const { store } = createTestStore({ count: 0 });\n\n    expect(store.get(\"/count\")).toBe(0);\n\n    store.set(\"/count\", 42);\n\n    expect(store.get(\"/count\")).toBe(42);\n    expect(store.getSnapshot().count).toBe(42);\n  });\n\n  it(\"update round-trip with multiple values\", () => {\n    const { store } = createTestStore({});\n\n    store.update({ \"/a\": 1, \"/b\": \"hello\" });\n\n    expect(store.get(\"/a\")).toBe(1);\n    expect(store.get(\"/b\")).toBe(\"hello\");\n    expect(store.getSnapshot()).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"subscribe fires on set\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"subscribe fires on update\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"unsubscribe stops notifications\", () => {\n    const { store } = createTestStore({});\n    const listener = vi.fn();\n    const unsub = store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n    expect(listener).toHaveBeenCalledTimes(1);\n\n    unsub();\n    store.set(\"/x\", 2);\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"getSnapshot immutability -- previous snapshot is not mutated\", () => {\n    const { store } = createTestStore({\n      user: { name: \"Alice\", age: 30 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/user/name\", \"Bob\");\n    const snap2 = store.getSnapshot();\n\n    expect(snap1.user).toEqual({ name: \"Alice\", age: 30 });\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect(snap1.user).not.toBe(snap2.user);\n  });\n\n  it(\"structural sharing -- untouched branches keep references\", () => {\n    const { store } = createTestStore({\n      a: { x: 1 },\n      b: { y: 2 },\n    });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/a/x\", 99);\n    const snap2 = store.getSnapshot();\n\n    expect(snap2.b).toBe(snap1.b);\n    expect(snap2.a).not.toBe(snap1.a);\n  });\n\n  it(\"getServerSnapshot returns same as getSnapshot\", () => {\n    const { store } = createTestStore({ x: 1 });\n\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n\n    store.set(\"/x\", 2);\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n  });\n\n  it(\"set skips update when value is unchanged\", () => {\n    const { store } = createTestStore({ x: 1 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"update skips update when no values changed\", () => {\n    const { store } = createTestStore({ a: 1, b: 2 });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"reads from the shared atom\", () => {\n    const atom = createAtom<Record<string, unknown>>({ count: 0 });\n    const store = xstateStoreStateStore({ atom });\n\n    atom.set({ count: 99 });\n\n    expect(store.get(\"/count\")).toBe(99);\n    expect(store.getSnapshot().count).toBe(99);\n  });\n\n  it(\"subscribe fires on external atom.set\", () => {\n    const atom = createAtom<Record<string, unknown>>({ count: 0 });\n    const store = xstateStoreStateStore({ atom });\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    atom.set({ count: 99 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n    expect(store.get(\"/count\")).toBe(99);\n  });\n});\n"
  },
  {
    "path": "packages/xstate/src/index.ts",
    "content": "import type { StateModel, StateStore } from \"@json-render/core\";\nimport { createStoreAdapter } from \"@json-render/core/store-utils\";\nimport type { Atom } from \"@xstate/store\";\n\nexport type { StateStore } from \"@json-render/core\";\n\n/**\n * Options for {@link xstateStoreStateStore}.\n */\nexport interface XstateStoreStateStoreOptions {\n  /** An `@xstate/store` atom (created with `createAtom`). */\n  atom: Atom<StateModel>;\n}\n\n/**\n * Create a {@link StateStore} backed by an `@xstate/store` atom.\n *\n * @example\n * ```ts\n * import { createAtom } from \"@xstate/store\";\n * import { xstateStoreStateStore } from \"@json-render/xstate\";\n *\n * const uiAtom = createAtom<Record<string, unknown>>({ count: 0 });\n *\n * const store = xstateStoreStateStore({ atom: uiAtom });\n *\n * <StateProvider store={store}>...</StateProvider>\n * ```\n */\nexport function xstateStoreStateStore(\n  options: XstateStoreStateStoreOptions,\n): StateStore {\n  const { atom } = options;\n\n  return createStoreAdapter({\n    getSnapshot: () => atom.get(),\n    setSnapshot: (next) => atom.set(next),\n    subscribe(listener) {\n      const sub = atom.subscribe(() => {\n        listener();\n      });\n      return () => sub.unsubscribe();\n    },\n  });\n}\n"
  },
  {
    "path": "packages/xstate/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/xstate/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\n    \"@json-render/core\",\n    \"@json-render/core/store-utils\",\n    \"@xstate/store\",\n  ],\n});\n"
  },
  {
    "path": "packages/yaml/CHANGELOG.md",
    "content": "# @json-render/yaml\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Minor Changes\n\n- a8afd8b: Add YAML wire format package and universal edit modes for surgical spec refinement.\n\n  ### New:\n  - **`@json-render/yaml`** -- YAML wire format for json-render. Includes streaming YAML parser, `yamlPrompt()` for system prompts, and AI SDK transform (`pipeYamlRender`) as a drop-in alternative to JSONL streaming. Supports four fence types: `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff`.\n  - **Universal edit modes** in `@json-render/core` -- three strategies for multi-turn spec refinement: `patch` (RFC 6902), `merge` (RFC 7396), and `diff` (unified diff). New `editModes` option on `buildUserPrompt()` and `PromptOptions`. New helpers: `deepMergeSpec()`, `diffToPatches()`, `buildEditUserPrompt()`, `buildEditInstructions()`, `isNonEmptySpec()`.\n\n  ### Improved:\n  - **Playground** -- format toggle (JSONL / YAML), edit mode picker (patch / merge / diff), and token usage display with prompt caching stats.\n  - **Prompt caching** -- generate API uses Anthropic ephemeral cache control for system prompts.\n  - **CI** -- lint, type-check, and test jobs now run in parallel.\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n"
  },
  {
    "path": "packages/yaml/README.md",
    "content": "# @json-render/yaml\n\nYAML wire format for `@json-render/core`. Progressive rendering and surgical edits via streaming YAML.\n\n## Installation\n\n```bash\nnpm install @json-render/yaml @json-render/core yaml\n```\n\n## Key Concepts\n\n- **YAML wire format**: Uses `yaml-spec`, `yaml-edit`, `yaml-patch`, and `diff` code fences instead of JSONL\n- **Streaming parser**: Incrementally parses YAML as it arrives, emitting JSON Patch operations\n- **Edit modes**: Supports patch (RFC 6902), merge (RFC 7396), and unified diff for surgical edits\n- **AI SDK transform**: Drop-in `TransformStream` that converts YAML fences into json-render patch data parts\n\n## Quick Start\n\n### Generate a YAML System Prompt\n\n```typescript\nimport { yamlPrompt } from \"@json-render/yaml\";\nimport { catalog } from \"./catalog\";\n\nconst systemPrompt = yamlPrompt(catalog, {\n  mode: \"standalone\",\n  editModes: [\"merge\"],\n});\n```\n\n### Stream YAML Specs (AI SDK)\n\n```typescript\nimport { pipeYamlRender } from \"@json-render/yaml\";\nimport { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeYamlRender(result.toUIMessageStream()));\n  },\n});\nreturn createUIMessageStreamResponse({ stream });\n```\n\n### Streaming Parser (Low-Level)\n\n```typescript\nimport { createYamlStreamCompiler } from \"@json-render/yaml\";\n\nconst compiler = createYamlStreamCompiler<Spec>();\n\n// Feed chunks as they arrive\nconst { result, newPatches } = compiler.push(\"root: main\\n\");\ncompiler.push(\"elements:\\n  main:\\n    type: Card\\n\");\n\n// Flush remaining data\nconst { result: final } = compiler.flush();\n```\n\n## API Reference\n\n### `yamlPrompt(catalog, options?)`\n\nGenerate a YAML-format system prompt from any json-render catalog.\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `system` | `string` | `\"You are a UI generator that outputs YAML.\"` | Custom system message intro |\n| `mode` | `\"standalone\" \\| \"inline\"` | `\"standalone\"` | Output mode |\n| `customRules` | `string[]` | `[]` | Additional rules |\n| `editModes` | `EditMode[]` | `[\"merge\"]` | Edit modes to document |\n\n### `createYamlTransform(options?)`\n\nCreates a `TransformStream` that converts YAML spec/edit fences in AI SDK stream chunks into json-render patch data parts.\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `previousSpec` | `Spec` | Seed with a previous spec for multi-turn edit support |\n\n### `pipeYamlRender(stream, options?)`\n\nConvenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.\n\n### `createYamlStreamCompiler(initial?)`\n\nCreate a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations.\n\n**Returns** `YamlStreamCompiler<T>` with methods:\n\n| Method | Description |\n|--------|-------------|\n| `push(chunk)` | Push a chunk of text. Returns `{ result, newPatches }` |\n| `flush()` | Flush remaining buffer and return final result |\n| `getResult()` | Get the current compiled result |\n| `getPatches()` | Get all patches applied so far |\n| `reset(initial?)` | Reset to initial state |\n\n### Fence Constants\n\nExported constants for fence detection:\n\n- `YAML_SPEC_FENCE` — `` ```yaml-spec ``\n- `YAML_EDIT_FENCE` — `` ```yaml-edit ``\n- `YAML_PATCH_FENCE` — `` ```yaml-patch ``\n- `DIFF_FENCE` — `` ```diff ``\n- `FENCE_CLOSE` — `` ``` ``\n\n### Re-exports from `@json-render/core`\n\n- `diffToPatches(oldObj, newObj)` — Generate RFC 6902 JSON Patch from object diff\n- `deepMergeSpec(base, patch)` — RFC 7396 JSON Merge Patch\n"
  },
  {
    "path": "packages/yaml/package.json",
    "content": "{\n  \"name\": \"@json-render/yaml\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.\",\n  \"keywords\": [\n    \"json\",\n    \"yaml\",\n    \"ui\",\n    \"ai\",\n    \"generative-ui\",\n    \"llm\",\n    \"streaming\",\n    \"progressive-rendering\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/yaml\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\",\n    \"diff\": \"^8.0.3\",\n    \"yaml\": \"^2.8.2\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"vitest\": \"^4.0.17\",\n    \"zod\": \"^4.3.6\"\n  }\n}\n"
  },
  {
    "path": "packages/yaml/src/diff.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { diffToPatches } from \"./diff\";\n\ndescribe(\"diffToPatches\", () => {\n  it(\"returns empty array for identical objects\", () => {\n    const obj = { a: 1, b: \"hello\" };\n    expect(diffToPatches(obj, obj)).toEqual([]);\n  });\n\n  it(\"detects added keys\", () => {\n    const patches = diffToPatches({}, { name: \"Alice\" });\n    expect(patches).toEqual([{ op: \"add\", path: \"/name\", value: \"Alice\" }]);\n  });\n\n  it(\"detects removed keys\", () => {\n    const patches = diffToPatches({ name: \"Alice\" }, {});\n    expect(patches).toEqual([{ op: \"remove\", path: \"/name\" }]);\n  });\n\n  it(\"detects changed scalar values\", () => {\n    const patches = diffToPatches({ name: \"Alice\" }, { name: \"Bob\" });\n    expect(patches).toEqual([{ op: \"replace\", path: \"/name\", value: \"Bob\" }]);\n  });\n\n  it(\"recurses into nested objects\", () => {\n    const oldObj = { user: { name: \"Alice\", age: 30 } };\n    const newObj = { user: { name: \"Alice\", age: 31 } };\n    const patches = diffToPatches(oldObj, newObj);\n    expect(patches).toEqual([{ op: \"replace\", path: \"/user/age\", value: 31 }]);\n  });\n\n  it(\"replaces arrays atomically\", () => {\n    const oldObj = { items: [\"a\", \"b\"] };\n    const newObj = { items: [\"a\", \"b\", \"c\"] };\n    const patches = diffToPatches(oldObj, newObj);\n    expect(patches).toEqual([\n      { op: \"replace\", path: \"/items\", value: [\"a\", \"b\", \"c\"] },\n    ]);\n  });\n\n  it(\"does not emit patch for identical arrays\", () => {\n    const oldObj = { items: [1, 2, 3] };\n    const newObj = { items: [1, 2, 3] };\n    expect(diffToPatches(oldObj, newObj)).toEqual([]);\n  });\n\n  it(\"handles type changes (object → scalar)\", () => {\n    const oldObj = { data: { nested: true } };\n    const newObj = { data: \"flat\" };\n    const patches = diffToPatches(\n      oldObj as Record<string, unknown>,\n      newObj as Record<string, unknown>,\n    );\n    expect(patches).toEqual([{ op: \"replace\", path: \"/data\", value: \"flat\" }]);\n  });\n\n  it(\"handles a complex spec diff\", () => {\n    const oldSpec = {\n      root: \"main\",\n      elements: {\n        main: { type: \"Card\", props: { title: \"Hello\" }, children: [] },\n      },\n    };\n    const newSpec = {\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Card\",\n          props: { title: \"Hello\" },\n          children: [\"child-1\"],\n        },\n        \"child-1\": {\n          type: \"Text\",\n          props: { content: \"World\" },\n          children: [],\n        },\n      },\n    };\n    const patches = diffToPatches(oldSpec, newSpec);\n    expect(patches).toContainEqual({\n      op: \"replace\",\n      path: \"/elements/main/children\",\n      value: [\"child-1\"],\n    });\n    expect(patches).toContainEqual({\n      op: \"add\",\n      path: \"/elements/child-1\",\n      value: {\n        type: \"Text\",\n        props: { content: \"World\" },\n        children: [],\n      },\n    });\n  });\n\n  it(\"escapes JSON Pointer tokens (~ and /)\", () => {\n    const patches = diffToPatches({}, { \"a/b\": 1, \"c~d\": 2 });\n    expect(patches).toContainEqual({\n      op: \"add\",\n      path: \"/a~1b\",\n      value: 1,\n    });\n    expect(patches).toContainEqual({\n      op: \"add\",\n      path: \"/c~0d\",\n      value: 2,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/yaml/src/diff.ts",
    "content": "export { diffToPatches } from \"@json-render/core\";\n"
  },
  {
    "path": "packages/yaml/src/index.ts",
    "content": "// Diff\nexport { diffToPatches } from \"./diff\";\n\n// Merge\nexport { deepMergeSpec } from \"./merge\";\n\n// Streaming YAML compiler\nexport type { YamlStreamCompiler } from \"./parser\";\nexport { createYamlStreamCompiler } from \"./parser\";\n\n// AI SDK transform\nexport {\n  createYamlTransform,\n  pipeYamlRender,\n  YAML_SPEC_FENCE,\n  YAML_EDIT_FENCE,\n  YAML_PATCH_FENCE,\n  DIFF_FENCE,\n  FENCE_CLOSE,\n} from \"./transform\";\n\n// Prompt generation\nexport type { YamlPromptOptions } from \"./prompt\";\nexport { yamlPrompt } from \"./prompt\";\n"
  },
  {
    "path": "packages/yaml/src/merge.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { deepMergeSpec } from \"./merge\";\n\ndescribe(\"deepMergeSpec\", () => {\n  it(\"adds new keys\", () => {\n    const result = deepMergeSpec({ a: 1 }, { b: 2 });\n    expect(result).toEqual({ a: 1, b: 2 });\n  });\n\n  it(\"replaces scalar values\", () => {\n    const result = deepMergeSpec({ a: 1 }, { a: 2 });\n    expect(result).toEqual({ a: 2 });\n  });\n\n  it(\"deletes keys set to null\", () => {\n    const result = deepMergeSpec({ a: 1, b: 2 }, { b: null });\n    expect(result).toEqual({ a: 1 });\n  });\n\n  it(\"deep-merges nested objects\", () => {\n    const base = { user: { name: \"Alice\", age: 30 } };\n    const patch = { user: { age: 31 } };\n    const result = deepMergeSpec(base, patch);\n    expect(result).toEqual({ user: { name: \"Alice\", age: 31 } });\n  });\n\n  it(\"replaces arrays (does not concat)\", () => {\n    const base = { items: [1, 2, 3] };\n    const patch = { items: [4, 5] };\n    const result = deepMergeSpec(base, patch);\n    expect(result).toEqual({ items: [4, 5] });\n  });\n\n  it(\"does not mutate base or patch\", () => {\n    const base = { a: { b: 1 } };\n    const patch = { a: { c: 2 } };\n    const baseCopy = JSON.parse(JSON.stringify(base));\n    const patchCopy = JSON.parse(JSON.stringify(patch));\n    deepMergeSpec(base, patch);\n    expect(base).toEqual(baseCopy);\n    expect(patch).toEqual(patchCopy);\n  });\n\n  it(\"handles a spec-like edit merge\", () => {\n    const base = {\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Card\",\n          props: { title: \"Dashboard\" },\n          children: [\"metric-1\"],\n        },\n        \"metric-1\": {\n          type: \"Metric\",\n          props: { label: \"Revenue\", value: \"$1M\" },\n          children: [],\n        },\n      },\n    };\n    const patch = {\n      elements: {\n        main: {\n          props: { title: \"Updated Dashboard\" },\n          children: [\"metric-1\", \"chart-1\"],\n        },\n        \"chart-1\": {\n          type: \"Chart\",\n          props: { data: \"revenue\" },\n          children: [],\n        },\n      },\n    };\n    const result = deepMergeSpec(base, patch);\n    expect(result.root).toBe(\"main\");\n    expect(\n      (result.elements as Record<string, Record<string, unknown>>)[\"main\"],\n    ).toEqual({\n      type: \"Card\",\n      props: { title: \"Updated Dashboard\" },\n      children: [\"metric-1\", \"chart-1\"],\n    });\n    expect(\n      (result.elements as Record<string, Record<string, unknown>>)[\"metric-1\"],\n    ).toEqual({\n      type: \"Metric\",\n      props: { label: \"Revenue\", value: \"$1M\" },\n      children: [],\n    });\n    expect(\n      (result.elements as Record<string, Record<string, unknown>>)[\"chart-1\"],\n    ).toEqual({\n      type: \"Chart\",\n      props: { data: \"revenue\" },\n      children: [],\n    });\n  });\n\n  it(\"deletes an element via null\", () => {\n    const base = {\n      elements: {\n        main: { type: \"Card\" },\n        old: { type: \"Widget\" },\n      },\n    };\n    const patch = { elements: { old: null } };\n    const result = deepMergeSpec(base, patch);\n    expect(result.elements).toEqual({ main: { type: \"Card\" } });\n  });\n});\n"
  },
  {
    "path": "packages/yaml/src/merge.ts",
    "content": "export { deepMergeSpec } from \"@json-render/core\";\n"
  },
  {
    "path": "packages/yaml/src/parser.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { createYamlStreamCompiler } from \"./parser\";\n\ndescribe(\"createYamlStreamCompiler\", () => {\n  it(\"parses a simple YAML document incrementally\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    const r1 = compiler.push(\"root: main\\n\");\n    expect(r1.newPatches.length).toBeGreaterThan(0);\n    expect(r1.result).toHaveProperty(\"root\", \"main\");\n  });\n\n  it(\"accumulates elements as lines arrive\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    compiler.push(\"root: main\\n\");\n    compiler.push(\"elements:\\n\");\n    compiler.push(\"  main:\\n\");\n    compiler.push(\"    type: Card\\n\");\n\n    const { result } = compiler.flush();\n    expect(result).toHaveProperty(\"root\", \"main\");\n\n    const elements = result.elements as Record<string, Record<string, unknown>>;\n    expect(elements.main).toBeDefined();\n    expect(elements.main!.type).toBe(\"Card\");\n  });\n\n  it(\"emits patches only for changes\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    const r1 = compiler.push(\"root: main\\n\");\n    expect(r1.newPatches).toEqual([\n      { op: \"add\", path: \"/root\", value: \"main\" },\n    ]);\n\n    // Pushing the same content again (no new complete line) should not emit patches\n    const r2 = compiler.push(\"\");\n    expect(r2.newPatches).toEqual([]);\n  });\n\n  it(\"tracks all patches via getPatches()\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    compiler.push(\"a: 1\\n\");\n    compiler.push(\"b: 2\\n\");\n\n    const allPatches = compiler.getPatches();\n    expect(allPatches.length).toBe(2);\n    expect(allPatches[0]).toEqual({ op: \"add\", path: \"/a\", value: 1 });\n    expect(allPatches[1]).toEqual({ op: \"add\", path: \"/b\", value: 2 });\n  });\n\n  it(\"resets to initial state\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    compiler.push(\"root: main\\n\");\n    expect(compiler.getResult()).toHaveProperty(\"root\", \"main\");\n\n    compiler.reset();\n    expect(compiler.getResult()).toEqual({});\n    expect(compiler.getPatches()).toEqual([]);\n  });\n\n  it(\"resets with initial value and diffs from it\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    compiler.reset({ root: \"existing\", elements: {} });\n\n    // The YAML includes root, so the initial value is preserved in the diff base\n    const { newPatches } = compiler.push(\n      \"root: existing\\nelements:\\n  main:\\n    type: Card\\n\",\n    );\n    const { result } = compiler.flush();\n\n    expect(result).toHaveProperty(\"root\", \"existing\");\n    expect(result).toHaveProperty(\"elements\");\n    // Only the new element should be patched, not root (unchanged)\n    expect(newPatches.find((p) => p.path === \"/root\")).toBeUndefined();\n    expect(newPatches.find((p) => p.path === \"/elements/main\")).toBeDefined();\n  });\n\n  it(\"handles a full spec YAML\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    const yaml = [\n      \"root: main\\n\",\n      \"elements:\\n\",\n      \"  main:\\n\",\n      \"    type: Card\\n\",\n      \"    props:\\n\",\n      \"      title: Dashboard\\n\",\n      \"    children:\\n\",\n      \"      - metric-1\\n\",\n      \"  metric-1:\\n\",\n      \"    type: Metric\\n\",\n      \"    props:\\n\",\n      \"      label: Revenue\\n\",\n      '      value: \"$1.2M\"\\n',\n      \"    children: []\\n\",\n      \"state:\\n\",\n      \"  revenue: 1200000\\n\",\n    ];\n\n    for (const line of yaml) {\n      compiler.push(line);\n    }\n    const { result } = compiler.flush();\n\n    expect(result.root).toBe(\"main\");\n    expect(result.state).toEqual({ revenue: 1200000 });\n\n    const elements = result.elements as Record<string, Record<string, unknown>>;\n    expect(elements.main).toBeDefined();\n    expect(elements[\"metric-1\"]).toBeDefined();\n    expect((elements[\"metric-1\"]!.props as Record<string, unknown>).label).toBe(\n      \"Revenue\",\n    );\n  });\n\n  it(\"does not crash on invalid YAML mid-stream\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    // Partial YAML that won't parse\n    compiler.push(\"elements:\\n\");\n    compiler.push(\"  main:\\n\");\n    compiler.push(\"    type: \"); // incomplete value — no newline yet\n\n    // Should not throw, result should still be from last successful parse\n    const r = compiler.push(\"\\n\");\n    expect(r.result).toBeDefined();\n  });\n\n  it(\"YAML 1.2 does not coerce yes/no/on/off to booleans\", () => {\n    const compiler = createYamlStreamCompiler();\n\n    compiler.push(\"active: yes\\n\");\n    compiler.push(\"disabled: no\\n\");\n    compiler.push(\"on_value: on\\n\");\n    compiler.push(\"off_value: off\\n\");\n\n    const { result } = compiler.flush();\n    // YAML 1.2 (yaml v2 default) treats these as strings, not booleans\n    expect(result.active).toBe(\"yes\");\n    expect(result.disabled).toBe(\"no\");\n    expect(result.on_value).toBe(\"on\");\n    expect(result.off_value).toBe(\"off\");\n  });\n});\n"
  },
  {
    "path": "packages/yaml/src/parser.ts",
    "content": "import { parse } from \"yaml\";\nimport type { JsonPatch } from \"@json-render/core\";\nimport { diffToPatches } from \"./diff\";\n\n/**\n * Streaming YAML compiler that incrementally parses YAML text and emits\n * JSON Patch operations for each change detected.\n *\n * Same interface shape as `SpecStreamCompiler` from `@json-render/core`.\n */\nexport interface YamlStreamCompiler<T> {\n  /** Push a chunk of text. Returns the current result and any new patches. */\n  push(chunk: string): { result: T; newPatches: JsonPatch[] };\n  /** Flush remaining buffer and return the final result. */\n  flush(): { result: T; newPatches: JsonPatch[] };\n  /** Get the current compiled result. */\n  getResult(): T;\n  /** Get all patches that have been applied. */\n  getPatches(): JsonPatch[];\n  /** Reset the compiler to initial state. */\n  reset(initial?: Partial<T>): void;\n}\n\n/**\n * Create a streaming YAML compiler.\n *\n * Incrementally parses YAML text as it arrives and emits JSON Patch\n * operations by diffing each successful parse against the previous snapshot.\n *\n * Uses `yaml.parse()` with YAML 1.2 defaults (the `yaml` v2 default).\n * YAML 1.2 does not coerce `yes`/`no`/`on`/`off` to booleans.\n *\n * @example\n * ```ts\n * const compiler = createYamlStreamCompiler<Spec>();\n * compiler.push(\"root: main\\n\");\n * compiler.push(\"elements:\\n  main:\\n    type: Card\\n\");\n * const { result } = compiler.flush();\n * ```\n */\nexport function createYamlStreamCompiler<\n  T extends Record<string, unknown> = Record<string, unknown>,\n>(initial?: Partial<T>): YamlStreamCompiler<T> {\n  let accumulated = \"\";\n  let snapshot: Record<string, unknown> = initial\n    ? { ...initial }\n    : ({} as Record<string, unknown>);\n  let result: T = { ...snapshot } as T;\n  const allPatches: JsonPatch[] = [];\n\n  function tryParse(): { result: T; newPatches: JsonPatch[] } {\n    const newPatches: JsonPatch[] = [];\n\n    try {\n      const parsed = parse(accumulated);\n\n      if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n        const patches = diffToPatches(\n          snapshot,\n          parsed as Record<string, unknown>,\n        );\n\n        if (patches.length > 0) {\n          snapshot = structuredClone(parsed as Record<string, unknown>);\n          result = { ...snapshot } as T;\n          allPatches.push(...patches);\n          newPatches.push(...patches);\n        }\n      }\n    } catch {\n      // Incomplete YAML — wait for more data\n    }\n\n    return { result, newPatches };\n  }\n\n  return {\n    push(chunk: string): { result: T; newPatches: JsonPatch[] } {\n      accumulated += chunk;\n\n      // Only attempt parse when we have a complete line\n      if (chunk.includes(\"\\n\")) {\n        return tryParse();\n      }\n\n      return { result, newPatches: [] };\n    },\n\n    flush(): { result: T; newPatches: JsonPatch[] } {\n      return tryParse();\n    },\n\n    getResult(): T {\n      return result;\n    },\n\n    getPatches(): JsonPatch[] {\n      return [...allPatches];\n    },\n\n    reset(newInitial?: Partial<T>): void {\n      accumulated = \"\";\n      snapshot = newInitial\n        ? { ...newInitial }\n        : ({} as Record<string, unknown>);\n      result = { ...snapshot } as T;\n      allPatches.length = 0;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/yaml/src/prompt.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { defineSchema, defineCatalog } from \"@json-render/core\";\nimport { z } from \"zod\";\nimport { yamlPrompt } from \"./prompt\";\n\nconst testSchema = defineSchema(\n  (s) => ({\n    spec: s.object({\n      root: s.string(),\n      elements: s.record(\n        s.object({\n          type: s.ref(\"catalog.components\"),\n          props: s.propsOf(\"catalog.components\"),\n          children: s.array(s.string()),\n        }),\n      ),\n    }),\n    catalog: s.object({\n      components: s.map({\n        props: s.zod(),\n        description: s.string(),\n      }),\n      actions: s.map({\n        description: s.string(),\n      }),\n    }),\n  }),\n  {\n    builtInActions: [{ name: \"setState\", description: \"Set a state value\" }],\n  },\n);\n\nconst testCatalog = defineCatalog(testSchema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string() }),\n      description: \"A card container\",\n    },\n    Text: {\n      props: z.object({ content: z.string() }),\n      description: \"Display text\",\n    },\n  },\n  actions: {\n    refresh: { description: \"Refresh data\" },\n  },\n});\n\ndescribe(\"yamlPrompt\", () => {\n  it(\"generates a prompt string\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(typeof prompt).toBe(\"string\");\n    expect(prompt.length).toBeGreaterThan(0);\n  });\n\n  it(\"includes YAML format instructions\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(prompt).toContain(\"YAML\");\n    expect(prompt).toContain(\"yaml-spec\");\n  });\n\n  it(\"includes component names from the catalog\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(prompt).toContain(\"Card\");\n    expect(prompt).toContain(\"Text\");\n  });\n\n  it(\"includes action names\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(prompt).toContain(\"refresh\");\n    expect(prompt).toContain(\"setState\");\n  });\n\n  it(\"includes yaml-edit instructions\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(prompt).toContain(\"yaml-edit\");\n    expect(prompt).toContain(\"deep merge\");\n  });\n\n  it(\"includes a YAML example\", () => {\n    const prompt = yamlPrompt(testCatalog);\n    expect(prompt).toContain(\"root: main\");\n    expect(prompt).toContain(\"elements:\");\n    expect(prompt).toContain(\"type: Card\");\n  });\n\n  it(\"respects mode: inline\", () => {\n    const prompt = yamlPrompt(testCatalog, { mode: \"inline\" });\n    expect(prompt).toContain(\"respond conversationally\");\n  });\n\n  it(\"respects mode: standalone\", () => {\n    const prompt = yamlPrompt(testCatalog, { mode: \"standalone\" });\n    expect(prompt).toContain(\"Output ONLY\");\n  });\n\n  it(\"appends custom rules\", () => {\n    const prompt = yamlPrompt(testCatalog, {\n      customRules: [\"Always use dark theme colors\"],\n    });\n    expect(prompt).toContain(\"Always use dark theme colors\");\n  });\n\n  it(\"uses custom system message\", () => {\n    const prompt = yamlPrompt(testCatalog, {\n      system: \"You are a dashboard builder.\",\n    });\n    expect(prompt).toContain(\"You are a dashboard builder.\");\n  });\n});\n"
  },
  {
    "path": "packages/yaml/src/prompt.ts",
    "content": "import { stringify } from \"yaml\";\nimport type { Catalog, EditMode, SchemaDefinition } from \"@json-render/core\";\nimport { buildEditInstructions } from \"@json-render/core\";\n\ninterface ZodLike {\n  _def?: Record<string, unknown>;\n}\n\nexport interface YamlPromptOptions {\n  /** Custom system message intro. */\n  system?: string;\n  /**\n   * - `\"standalone\"` (default): LLM outputs only the YAML spec (no prose).\n   * - `\"inline\"`: LLM responds conversationally, then wraps YAML in a fence.\n   */\n  mode?: \"standalone\" | \"inline\";\n  /** Additional rules appended to the RULES section. */\n  customRules?: string[];\n  /** Edit modes to document. Default: `[\"merge\"]` (yaml-edit). */\n  editModes?: EditMode[];\n}\n\ninterface CatalogComponentDef {\n  props?: ZodLike;\n  description?: string;\n  slots?: string[];\n  events?: string[];\n  example?: Record<string, unknown>;\n}\n\n// ── Zod introspection (local, minimal) ──────────────────────────────────────\n\nfunction getZodTypeName(schema: ZodLike): string {\n  if (!schema?._def) return \"\";\n  const def = schema._def;\n  return (\n    (def.typeName as string | undefined) ??\n    (typeof def.type === \"string\" ? (def.type as string) : \"\") ??\n    \"\"\n  );\n}\n\nfunction formatZodType(schema: ZodLike): string {\n  if (!schema?._def) return \"unknown\";\n  const def = schema._def;\n  const typeName = getZodTypeName(schema);\n\n  switch (typeName) {\n    case \"ZodString\":\n    case \"string\":\n      return \"string\";\n    case \"ZodNumber\":\n    case \"number\":\n      return \"number\";\n    case \"ZodBoolean\":\n    case \"boolean\":\n      return \"boolean\";\n    case \"ZodLiteral\":\n    case \"literal\":\n      return JSON.stringify(def.value);\n    case \"ZodEnum\":\n    case \"enum\": {\n      let values: string[];\n      if (Array.isArray(def.values)) {\n        values = def.values as string[];\n      } else if (def.entries && typeof def.entries === \"object\") {\n        values = Object.values(def.entries as Record<string, string>);\n      } else {\n        return \"enum\";\n      }\n      return values.map((v) => `\"${v}\"`).join(\" | \");\n    }\n    case \"ZodArray\":\n    case \"array\": {\n      const inner = (\n        typeof def.element === \"object\"\n          ? def.element\n          : typeof def.type === \"object\"\n            ? def.type\n            : undefined\n      ) as ZodLike | undefined;\n      return inner ? `Array<${formatZodType(inner)}>` : \"Array<unknown>\";\n    }\n    case \"ZodObject\":\n    case \"object\": {\n      const shape =\n        typeof def.shape === \"function\"\n          ? (def.shape as () => Record<string, ZodLike>)()\n          : (def.shape as Record<string, ZodLike>);\n      if (!shape) return \"object\";\n      const props = Object.entries(shape)\n        .map(([key, value]) => {\n          const innerTypeName = getZodTypeName(value);\n          const isOptional =\n            innerTypeName === \"ZodOptional\" ||\n            innerTypeName === \"ZodNullable\" ||\n            innerTypeName === \"optional\" ||\n            innerTypeName === \"nullable\";\n          return `${key}${isOptional ? \"?\" : \"\"}: ${formatZodType(value)}`;\n        })\n        .join(\", \");\n      return `{ ${props} }`;\n    }\n    case \"ZodOptional\":\n    case \"optional\":\n    case \"ZodNullable\":\n    case \"nullable\": {\n      const inner = (def.innerType as ZodLike) ?? (def.wrapped as ZodLike);\n      return inner ? formatZodType(inner) : \"unknown\";\n    }\n    case \"ZodUnion\":\n    case \"union\": {\n      const options = def.options as ZodLike[] | undefined;\n      return options\n        ? options.map((opt) => formatZodType(opt)).join(\" | \")\n        : \"unknown\";\n    }\n    default:\n      return \"unknown\";\n  }\n}\n\nfunction getExampleProps(def: CatalogComponentDef): Record<string, unknown> {\n  if (def.example && Object.keys(def.example).length > 0) return def.example;\n  if (!def.props?._def) return {};\n  const zodDef = def.props._def;\n  const typeName = getZodTypeName(def.props);\n  if (typeName !== \"ZodObject\" && typeName !== \"object\") return {};\n  const shape =\n    typeof zodDef.shape === \"function\"\n      ? (zodDef.shape as () => Record<string, ZodLike>)()\n      : (zodDef.shape as Record<string, ZodLike>);\n  if (!shape) return {};\n  const result: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(shape)) {\n    const inner = getZodTypeName(value);\n    if (\n      inner === \"ZodOptional\" ||\n      inner === \"optional\" ||\n      inner === \"ZodNullable\" ||\n      inner === \"nullable\"\n    )\n      continue;\n    result[key] = exampleValue(value);\n  }\n  return result;\n}\n\nfunction exampleValue(schema: ZodLike): unknown {\n  if (!schema?._def) return \"...\";\n  const def = schema._def;\n  const t = getZodTypeName(schema);\n  switch (t) {\n    case \"ZodString\":\n    case \"string\":\n      return \"example\";\n    case \"ZodNumber\":\n    case \"number\":\n      return 0;\n    case \"ZodBoolean\":\n    case \"boolean\":\n      return true;\n    case \"ZodLiteral\":\n    case \"literal\":\n      return def.value;\n    case \"ZodEnum\":\n    case \"enum\": {\n      if (Array.isArray(def.values) && (def.values as unknown[]).length > 0)\n        return (def.values as unknown[])[0];\n      if (def.entries && typeof def.entries === \"object\") {\n        const vals = Object.values(def.entries as Record<string, string>);\n        return vals.length > 0 ? vals[0] : \"example\";\n      }\n      return \"example\";\n    }\n    case \"ZodOptional\":\n    case \"optional\":\n    case \"ZodNullable\":\n    case \"nullable\":\n    case \"ZodDefault\":\n    case \"default\": {\n      const inner = (def.innerType as ZodLike) ?? (def.wrapped as ZodLike);\n      return inner ? exampleValue(inner) : null;\n    }\n    case \"ZodArray\":\n    case \"array\":\n      return [];\n    case \"ZodObject\":\n    case \"object\":\n      return getExampleProps({ props: schema } as CatalogComponentDef);\n    case \"ZodUnion\":\n    case \"union\": {\n      const options = def.options as ZodLike[] | undefined;\n      return options && options.length > 0 ? exampleValue(options[0]!) : \"...\";\n    }\n    default:\n      return \"...\";\n  }\n}\n\n// ── YAML helper ──\n\n/** Render a value as an indented YAML string (2-space indent). */\nfunction toYaml(value: unknown): string {\n  return stringify(value, { indent: 2 }).trimEnd();\n}\n\n// ── Prompt generation ──\n\n/**\n * Generate a YAML-format system prompt from any json-render catalog.\n *\n * Works with catalogs from any renderer (`@json-render/react`,\n * `@json-render/vue`, etc.) — it only reads the catalog metadata.\n *\n * @example\n * ```ts\n * import { yamlPrompt } from \"@json-render/yaml\";\n * const systemPrompt = yamlPrompt(catalog, { mode: \"inline\" });\n * ```\n */\nexport function yamlPrompt(\n  catalog: Catalog<SchemaDefinition, unknown>,\n  options?: YamlPromptOptions,\n): string {\n  const {\n    system = \"You are a UI generator that outputs YAML.\",\n    mode = \"standalone\",\n    customRules = [],\n    editModes = [\"merge\"],\n  } = options ?? {};\n\n  const lines: string[] = [];\n  lines.push(system);\n  lines.push(\"\");\n\n  // ── Output format ──\n\n  if (mode === \"inline\") {\n    lines.push(\"OUTPUT FORMAT (text + YAML):\");\n    lines.push(\n      \"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output the YAML spec wrapped in a ```yaml-spec code fence.\",\n    );\n    lines.push(\n      \"If the user's message does not require a UI (e.g. a greeting or clarifying question), respond with text only — no YAML.\",\n    );\n  } else {\n    lines.push(\"OUTPUT FORMAT (YAML):\");\n    lines.push(\n      \"Output a YAML document that describes a UI tree. Wrap it in a ```yaml-spec code fence.\",\n    );\n  }\n  lines.push(\"\");\n  lines.push(\n    \"The YAML document has three top-level keys: root, elements, and state (optional).\",\n  );\n  lines.push(\n    \"Stream progressively — output elements one at a time so the UI fills in as you write.\",\n  );\n  lines.push(\"\");\n\n  // ── Example ──\n\n  const allComponents = (catalog.data as Record<string, unknown>).components as\n    | Record<string, CatalogComponentDef>\n    | undefined;\n  const cn = catalog.componentNames;\n  const comp1 = cn[0] || \"Component\";\n  const comp2 = cn.length > 1 ? cn[1]! : comp1;\n  const comp1Def = allComponents?.[comp1];\n  const comp2Def = allComponents?.[comp2];\n  const comp1Props = comp1Def ? getExampleProps(comp1Def) : {};\n  const comp2Props = comp2Def ? getExampleProps(comp2Def) : {};\n\n  const exampleSpec = {\n    root: \"main\",\n    elements: {\n      main: {\n        type: comp1,\n        props: comp1Props,\n        children: [\"child-1\", \"list\"],\n      },\n      \"child-1\": {\n        type: comp2,\n        props: comp2Props,\n        children: [],\n      },\n      list: {\n        type: comp1,\n        props: comp1Props,\n        repeat: { statePath: \"/items\", key: \"id\" },\n        children: [\"item\"],\n      },\n      item: {\n        type: comp2,\n        props: { ...comp2Props },\n        children: [],\n      },\n    },\n    state: {\n      items: [\n        { id: \"1\", title: \"First Item\" },\n        { id: \"2\", title: \"Second Item\" },\n      ],\n    },\n  };\n\n  lines.push(\"Example:\");\n  lines.push(\"\");\n  lines.push(\"```yaml-spec\");\n  lines.push(toYaml(exampleSpec));\n  lines.push(\"```\");\n  lines.push(\"\");\n\n  // ── Edit modes (dynamic based on config) ──\n\n  lines.push(buildEditInstructions({ modes: editModes }, \"yaml\"));\n\n  // ── Initial state ──\n\n  lines.push(\"INITIAL STATE:\");\n  lines.push(\n    \"The spec includes a top-level `state` key to seed the state model. Components using $state, $bindState, $bindItem, $item, or $index read from this state.\",\n  );\n  lines.push(\n    \"CRITICAL: You MUST include state whenever your UI displays data via these expressions or uses repeat to iterate over arrays.\",\n  );\n  lines.push(\n    \"Include realistic sample data. For lists: 3-5 items with relevant fields. Never leave arrays empty.\",\n  );\n  lines.push(\n    'When content comes from the state model, use { \"$state\": \"/some/path\" } dynamic props instead of hardcoding values.',\n  );\n  lines.push(\n    'State paths use RFC 6901 JSON Pointer syntax (e.g. \"/todos/0/title\"). Do NOT use dot notation.',\n  );\n  lines.push(\"\");\n\n  // ── Dynamic lists ──\n\n  lines.push(\"DYNAMIC LISTS (repeat field):\");\n  lines.push(\n    \"Any element can have a top-level `repeat` field to render its children once per item in a state array.\",\n  );\n  lines.push(\"Example:\");\n  lines.push(\"\");\n  lines.push(\n    toYaml({\n      list: {\n        type: comp1,\n        props: comp1Props,\n        repeat: { statePath: \"/todos\", key: \"id\" },\n        children: [\"todo-item\"],\n      },\n    }),\n  );\n  lines.push(\"\");\n  lines.push(\n    'Inside repeated children, use { \"$item\": \"field\" } for item data and { \"$index\": true } for the array index.',\n  );\n  lines.push(\n    \"ALWAYS use repeat for lists backed by state arrays. NEVER hardcode individual elements per item.\",\n  );\n  lines.push(\n    \"IMPORTANT: `repeat` is a top-level field on the element (sibling of type/props/children), NOT inside props.\",\n  );\n  lines.push(\"\");\n\n  // ── Array state actions ──\n\n  lines.push(\"ARRAY STATE ACTIONS:\");\n  lines.push(\n    'Use \"pushState\" to append items to arrays. Use \"removeState\" to remove items by index.',\n  );\n  lines.push('Use \"$id\" inside pushState values to auto-generate a unique ID.');\n  lines.push(\"\");\n\n  // ── Components ──\n\n  if (allComponents) {\n    lines.push(`AVAILABLE COMPONENTS (${catalog.componentNames.length}):`);\n    lines.push(\"\");\n\n    for (const [name, def] of Object.entries(allComponents)) {\n      const propsStr = def.props ? formatZodType(def.props) : \"{}\";\n      const hasChildren = def.slots && def.slots.length > 0;\n      const childrenStr = hasChildren ? \" [accepts children]\" : \"\";\n      const eventsStr =\n        def.events && def.events.length > 0\n          ? ` [events: ${def.events.join(\", \")}]`\n          : \"\";\n      const descStr = def.description ? ` - ${def.description}` : \"\";\n      lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);\n    }\n    lines.push(\"\");\n  }\n\n  // ── Actions ──\n\n  const actions = (catalog.data as Record<string, unknown>).actions as\n    | Record<string, { params?: ZodLike; description?: string }>\n    | undefined;\n  const builtInActions = catalog.schema.builtInActions ?? [];\n  const hasCustomActions = actions && catalog.actionNames.length > 0;\n  const hasBuiltInActions = builtInActions.length > 0;\n\n  if (hasCustomActions || hasBuiltInActions) {\n    lines.push(\"AVAILABLE ACTIONS:\");\n    lines.push(\"\");\n    for (const action of builtInActions) {\n      lines.push(`- ${action.name}: ${action.description} [built-in]`);\n    }\n    if (hasCustomActions) {\n      for (const [name, def] of Object.entries(actions)) {\n        lines.push(`- ${name}${def.description ? `: ${def.description}` : \"\"}`);\n      }\n    }\n    lines.push(\"\");\n  }\n\n  // ── Events ──\n\n  lines.push(\"EVENTS (the `on` field):\");\n  lines.push(\n    \"Elements can have an optional `on` field to bind events to actions. It is a top-level field (sibling of type/props/children), NOT inside props.\",\n  );\n  lines.push(\"Example:\");\n  lines.push(\"\");\n  lines.push(\n    toYaml({\n      \"save-btn\": {\n        type: comp1,\n        props: comp1Props,\n        on: {\n          press: {\n            action: \"setState\",\n            params: { statePath: \"/saved\", value: true },\n          },\n        },\n        children: [],\n      },\n    }),\n  );\n  lines.push(\"\");\n  lines.push(\n    'Action params can use dynamic references: { \"$state\": \"/statePath\" }.',\n  );\n  lines.push(\n    \"IMPORTANT: Do NOT put action/actionParams inside props. Always use the `on` field.\",\n  );\n  lines.push(\"\");\n\n  // ── Visibility ──\n\n  lines.push(\"VISIBILITY CONDITIONS:\");\n  lines.push(\n    \"Elements can have an optional `visible` field to conditionally show/hide based on state. It is a top-level field (sibling of type/props/children).\",\n  );\n  lines.push(\"Conditions:\");\n  lines.push('- { \"$state\": \"/path\" } — visible when truthy');\n  lines.push('- { \"$state\": \"/path\", \"not\": true } — visible when falsy');\n  lines.push('- { \"$state\": \"/path\", \"eq\": \"value\" } — visible when equal');\n  lines.push(\n    '- { \"$state\": \"/path\", \"neq\": \"value\" } — visible when not equal',\n  );\n  lines.push(\n    '- { \"$state\": \"/path\", \"gt\": N } / gte / lt / lte — numeric comparisons',\n  );\n  lines.push(\"- Use ONE operator per condition. Do not combine multiple.\");\n  lines.push('- Add \"not\": true to any condition to invert it.');\n  lines.push(\"- [cond, cond] — implicit AND (all must be true)\");\n  lines.push('- { \"$and\": [...] } — explicit AND');\n  lines.push('- { \"$or\": [...] } — at least one must be true');\n  lines.push(\"- true / false — always visible/hidden\");\n  lines.push(\"\");\n\n  // ── Dynamic props ──\n\n  lines.push(\"DYNAMIC PROPS:\");\n  lines.push(\"Any prop value can be a dynamic expression:\");\n  lines.push(\n    '1. Read-only: { \"$state\": \"/path\" } — resolves to the value at that state path.',\n  );\n  lines.push(\n    '2. Two-way binding: { \"$bindState\": \"/path\" } — read + write. Use on form inputs.',\n  );\n  lines.push(\n    '   Inside repeat scopes: { \"$bindItem\": \"field\" } for item-level binding.',\n  );\n  lines.push(\n    '3. Conditional: { \"$cond\": <condition>, \"$then\": <val>, \"$else\": <val> }',\n  );\n  lines.push(\n    '4. Template: { \"$template\": \"Hello, ${/name}!\" } — interpolates state references.',\n  );\n  lines.push(\"\");\n\n  // ── $computed (only if catalog has functions) ──\n\n  const catalogFunctions = (catalog.data as Record<string, unknown>).functions;\n  if (catalogFunctions && Object.keys(catalogFunctions).length > 0) {\n    lines.push(\n      '5. Computed: { \"$computed\": \"<fn>\", \"args\": { \"key\": <expr> } }',\n    );\n    lines.push(\"   Available functions:\");\n    for (const name of Object.keys(\n      catalogFunctions as Record<string, unknown>,\n    )) {\n      lines.push(`   - ${name}`);\n    }\n    lines.push(\"\");\n  }\n\n  // ── Validation (only if components have checks) ──\n\n  const hasChecks = allComponents\n    ? Object.values(allComponents).some((def) => {\n        if (!def.props) return false;\n        return formatZodType(def.props).includes(\"checks\");\n      })\n    : false;\n\n  if (hasChecks) {\n    lines.push(\"VALIDATION:\");\n    lines.push(\n      \"Form components with a `checks` prop support client-side validation.\",\n    );\n    lines.push(\n      \"Built-in types: required, email, minLength, maxLength, pattern, min, max, numeric, url, matches, equalTo, lessThan, greaterThan, requiredIf.\",\n    );\n    lines.push(\n      \"IMPORTANT: Components with checks must also use $bindState or $bindItem for two-way binding.\",\n    );\n    lines.push(\"\");\n  }\n\n  // ── State watchers ──\n\n  if (hasCustomActions || hasBuiltInActions) {\n    lines.push(\"STATE WATCHERS:\");\n    lines.push(\n      \"Elements can have an optional `watch` field to trigger actions when state changes. Top-level field, NOT inside props.\",\n    );\n    lines.push(\n      \"Maps state paths to action bindings. Fires when the watched value changes (not on initial render).\",\n    );\n    lines.push(\"\");\n  }\n\n  // ── Rules ──\n\n  lines.push(\"RULES:\");\n  const baseRules =\n    mode === \"inline\"\n      ? [\n          \"When generating UI, wrap the YAML in a ```yaml-spec code fence\",\n          \"Write a brief conversational response before the YAML\",\n          \"When editing existing UI, use a ```yaml-edit fence with only the changed parts\",\n          \"The document must have: root (string), elements (map of elements), and optionally state\",\n          \"Each element must have: type, props, children (list of child keys)\",\n          \"ONLY use components listed above\",\n          \"Use unique, descriptive keys for elements (e.g. 'header', 'metric-1', 'chart-revenue')\",\n          \"Include state whenever using $state, $bindState, $bindItem, $item, $index, or repeat\",\n        ]\n      : [\n          \"Output ONLY the YAML spec inside a ```yaml-spec fence — no prose, no extra markdown\",\n          \"When editing existing UI, use a ```yaml-edit fence with only the changed parts\",\n          \"The document must have: root (string), elements (map of elements), and optionally state\",\n          \"Each element must have: type, props, children (list of child keys)\",\n          \"ONLY use components listed above\",\n          \"Use unique, descriptive keys for elements (e.g. 'header', 'metric-1', 'chart-revenue')\",\n          \"Include state whenever using $state, $bindState, $bindItem, $item, $index, or repeat\",\n        ];\n\n  const schemaRules = catalog.schema.defaultRules ?? [];\n  const allRules = [...baseRules, ...schemaRules, ...customRules];\n  allRules.forEach((rule, i) => {\n    lines.push(`${i + 1}. ${rule}`);\n  });\n\n  return lines.join(\"\\n\");\n}\n"
  },
  {
    "path": "packages/yaml/src/transform.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { SPEC_DATA_PART_TYPE, type StreamChunk } from \"@json-render/core\";\nimport { createYamlTransform } from \"./transform\";\n\n/** Helper: feed text chunks through the transform and collect output. */\nasync function runTransform(\n  lines: string[],\n  options?: Parameters<typeof createYamlTransform>[0],\n): Promise<StreamChunk[]> {\n  const transform = createYamlTransform(options);\n  const output: StreamChunk[] = [];\n\n  // Build input chunks\n  const inputChunks: StreamChunk[] = [\n    { type: \"text-start\", id: \"1\" },\n    ...lines.map((l) => ({ type: \"text-delta\" as const, id: \"1\", delta: l })),\n    { type: \"text-end\", id: \"1\" },\n  ];\n\n  // Create a readable from the input chunks and pipe through\n  const input = new ReadableStream<StreamChunk>({\n    start(controller) {\n      for (const chunk of inputChunks) {\n        controller.enqueue(chunk);\n      }\n      controller.close();\n    },\n  });\n\n  const outputStream = input.pipeThrough(transform);\n  const reader = outputStream.getReader();\n\n  while (true) {\n    const { done, value } = await reader.read();\n    if (done) break;\n    output.push(value);\n  }\n\n  return output;\n}\n\nfunction extractPatches(output: StreamChunk[]) {\n  return output\n    .filter((c) => c.type === SPEC_DATA_PART_TYPE)\n    .map((c) => (c as { data: { type: string; patch: unknown } }).data.patch);\n}\n\nfunction extractText(output: StreamChunk[]) {\n  return output\n    .filter((c) => c.type === \"text-delta\")\n    .map((c) => (c as { delta: string }).delta)\n    .join(\"\");\n}\n\ndescribe(\"createYamlTransform\", () => {\n  it(\"passes through plain text outside fences\", async () => {\n    const output = await runTransform([\"Hello world\\n\", \"How are you?\\n\"]);\n    const text = extractText(output);\n    expect(text).toContain(\"Hello world\");\n    expect(text).toContain(\"How are you?\");\n  });\n\n  it(\"parses yaml-spec fence into patches\", async () => {\n    const output = await runTransform([\n      \"Here is your UI:\\n\",\n      \"```yaml-spec\\n\",\n      \"root: main\\n\",\n      \"elements:\\n\",\n      \"  main:\\n\",\n      \"    type: Card\\n\",\n      \"    props:\\n\",\n      \"      title: Dashboard\\n\",\n      \"    children: []\\n\",\n      \"```\\n\",\n    ]);\n\n    const patches = extractPatches(output);\n    expect(patches.length).toBeGreaterThan(0);\n\n    // Should have patched the root\n    const rootPatch = patches.find(\n      (p: any) => p.path === \"/root\" && p.value === \"main\",\n    );\n    expect(rootPatch).toBeDefined();\n\n    // Text before the fence should pass through\n    const text = extractText(output);\n    expect(text).toContain(\"Here is your UI:\");\n  });\n\n  it(\"parses yaml-edit fence with merge semantics\", async () => {\n    const previousSpec = {\n      root: \"main\",\n      elements: {\n        main: {\n          type: \"Card\",\n          props: { title: \"Old Title\" },\n          children: [],\n        },\n      },\n    };\n\n    // Only send a yaml-edit block — previousSpec is the base\n    const output = await runTransform(\n      [\n        \"```yaml-edit\\n\",\n        \"elements:\\n\",\n        \"  main:\\n\",\n        \"    props:\\n\",\n        \"      title: New Title\\n\",\n        \"```\\n\",\n      ],\n      { previousSpec: previousSpec as any },\n    );\n\n    const patches = extractPatches(output);\n\n    // Should have at least one patch\n    expect(patches.length).toBeGreaterThan(0);\n\n    // Should include a patch that updates the title — could be at\n    // the leaf level or replacing the whole props object\n    const hasTitleUpdate = patches.some(\n      (p: any) =>\n        (p.path === \"/elements/main/props/title\" && p.value === \"New Title\") ||\n        (p.path === \"/elements/main/props\" &&\n          (p.value as any)?.title === \"New Title\"),\n    );\n    expect(hasTitleUpdate).toBe(true);\n  });\n\n  it(\"swallows fence delimiters (not emitted as text)\", async () => {\n    const output = await runTransform([\n      \"```yaml-spec\\n\",\n      \"root: main\\n\",\n      \"```\\n\",\n    ]);\n\n    const text = extractText(output);\n    expect(text).not.toContain(\"```yaml-spec\");\n    expect(text).not.toContain(\"```\");\n  });\n\n  it(\"handles non-text chunks by passing them through\", async () => {\n    const transform = createYamlTransform();\n    const input = new ReadableStream<StreamChunk>({\n      start(controller) {\n        controller.enqueue({ type: \"tool-call\", id: \"t1\", name: \"getWeather\" });\n        controller.close();\n      },\n    });\n\n    const reader = input.pipeThrough(transform).getReader();\n    const { value } = await reader.read();\n    expect(value).toEqual({\n      type: \"tool-call\",\n      id: \"t1\",\n      name: \"getWeather\",\n    });\n  });\n});\n"
  },
  {
    "path": "packages/yaml/src/transform.ts",
    "content": "import { parse, stringify } from \"yaml\";\nimport { applyPatch as applyUnifiedDiff } from \"diff\";\nimport {\n  SPEC_DATA_PART_TYPE,\n  deepMergeSpec,\n  diffToPatches,\n  type JsonPatch,\n  type Spec,\n  type StreamChunk,\n} from \"@json-render/core\";\nimport { createYamlStreamCompiler } from \"./parser\";\n\nexport const YAML_SPEC_FENCE = \"```yaml-spec\";\nexport const YAML_EDIT_FENCE = \"```yaml-edit\";\nexport const YAML_PATCH_FENCE = \"```yaml-patch\";\nexport const DIFF_FENCE = \"```diff\";\nexport const FENCE_CLOSE = \"```\";\n\nexport interface YamlTransformOptions {\n  /** Seed with a previous spec for multi-turn edit support. */\n  previousSpec?: Spec;\n}\n\n/**\n * Creates a `TransformStream` that intercepts AI SDK UI message stream chunks\n * and converts YAML spec/edit blocks into json-render patch data parts.\n *\n * Two fence types are recognised:\n *\n * 1. **`\\`\\`\\`yaml-spec`** — Full YAML spec. Parsed progressively, emitting\n *    patches as each new property is detected.\n * 2. **`\\`\\`\\`yaml-edit`** — Partial YAML. Deep-merged with the current spec,\n *    then diffed to produce patches. Only changed keys need to be included.\n *\n * Non-fenced text passes through unchanged as `text-delta` chunks, matching\n * the behaviour of `createJsonRenderTransform` from `@json-render/core`.\n */\nexport function createYamlTransform(\n  options?: YamlTransformOptions,\n): TransformStream<StreamChunk, StreamChunk> {\n  let currentTextId = \"\";\n  let inTextBlock = false;\n  let textIdCounter = 0;\n\n  let lineBuffer = \"\";\n  let buffering = false;\n\n  // Fence state\n  let fenceMode: \"yaml-spec\" | \"yaml-edit\" | \"yaml-patch\" | \"diff\" | null =\n    null;\n  let yamlAccumulated = \"\";\n  let diffAccumulated = \"\";\n\n  // Streaming compiler for yaml-spec progressive rendering\n  let compiler = createYamlStreamCompiler<Record<string, unknown>>();\n\n  // The \"current spec\" — built up during yaml-spec, used as base for yaml-edit\n  let currentSpec: Record<string, unknown> = options?.previousSpec\n    ? structuredClone(\n        options.previousSpec as unknown as Record<string, unknown>,\n      )\n    : {};\n\n  // ── Text block helpers (same pattern as createJsonRenderTransform) ──\n\n  function closeTextBlock(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (inTextBlock) {\n      controller.enqueue({ type: \"text-end\", id: currentTextId });\n      inTextBlock = false;\n    }\n  }\n\n  function ensureTextBlock(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (!inTextBlock) {\n      textIdCounter++;\n      currentTextId = String(textIdCounter);\n      controller.enqueue({ type: \"text-start\", id: currentTextId });\n      inTextBlock = true;\n    }\n  }\n\n  function emitTextDelta(\n    delta: string,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    ensureTextBlock(controller);\n    controller.enqueue({ type: \"text-delta\", id: currentTextId, delta });\n  }\n\n  function emitPatch(\n    patch: JsonPatch,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    closeTextBlock(controller);\n    controller.enqueue({\n      type: SPEC_DATA_PART_TYPE,\n      data: { type: \"patch\", patch },\n    });\n  }\n\n  function emitPatches(\n    patches: JsonPatch[],\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    for (const patch of patches) {\n      emitPatch(patch, controller);\n    }\n  }\n\n  // ── YAML fence processing ──\n\n  /**\n   * Feed a line of YAML to the streaming compiler (yaml-spec mode).\n   * Emits patches for any newly detected properties.\n   */\n  function feedYamlSpec(\n    line: string,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    yamlAccumulated += line + \"\\n\";\n    const { newPatches } = compiler.push(line + \"\\n\");\n    if (newPatches.length > 0) {\n      emitPatches(newPatches, controller);\n    }\n  }\n\n  /**\n   * Feed a line of YAML for edit mode. We accumulate all lines and process\n   * on fence close since partial edits may not parse until complete.\n   */\n  function feedYamlEdit(line: string) {\n    yamlAccumulated += line + \"\\n\";\n  }\n\n  /**\n   * Finalise a yaml-edit block: parse the accumulated YAML, deep-merge\n   * with the current spec, diff, and emit patches.\n   */\n  function finaliseYamlEdit(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    try {\n      const editObj = parse(yamlAccumulated);\n      if (editObj && typeof editObj === \"object\" && !Array.isArray(editObj)) {\n        const merged = deepMergeSpec(\n          currentSpec,\n          editObj as Record<string, unknown>,\n        );\n        const patches = diffToPatches(currentSpec, merged);\n        if (patches.length > 0) {\n          currentSpec = merged;\n          emitPatches(patches, controller);\n        }\n      }\n    } catch {\n      // Invalid YAML edit block — silently drop\n    }\n  }\n\n  /**\n   * Finalise a yaml-spec block: flush the compiler for any remaining\n   * partial data and update currentSpec.\n   */\n  function finaliseYamlSpec(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    const { newPatches } = compiler.flush();\n    if (newPatches.length > 0) {\n      emitPatches(newPatches, controller);\n    }\n    currentSpec = structuredClone(\n      compiler.getResult() as Record<string, unknown>,\n    );\n  }\n\n  /**\n   * Finalise a yaml-patch block: parse each accumulated line as an\n   * RFC 6902 JSON Patch operation and emit directly.\n   */\n  function finaliseYamlPatch(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    for (const line of yamlAccumulated.split(\"\\n\")) {\n      const trimmed = line.trim();\n      if (!trimmed) continue;\n      try {\n        const patch = JSON.parse(trimmed) as JsonPatch;\n        if (patch.op) {\n          emitPatch(patch, controller);\n          // Update currentSpec for subsequent edits\n          if (patch.op === \"add\" || patch.op === \"replace\") {\n            const parts = patch.path.split(\"/\").filter(Boolean);\n            let target: Record<string, unknown> = currentSpec;\n            for (let i = 0; i < parts.length - 1; i++) {\n              const key = parts[i]!;\n              if (typeof target[key] !== \"object\" || target[key] === null) {\n                target[key] = {};\n              }\n              target = target[key] as Record<string, unknown>;\n            }\n            const lastKey = parts[parts.length - 1];\n            if (lastKey) target[lastKey] = patch.value;\n          } else if (patch.op === \"remove\") {\n            const parts = patch.path.split(\"/\").filter(Boolean);\n            let target: Record<string, unknown> = currentSpec;\n            for (let i = 0; i < parts.length - 1; i++) {\n              const key = parts[i]!;\n              if (typeof target[key] !== \"object\" || target[key] === null)\n                break;\n              target = target[key] as Record<string, unknown>;\n            }\n            const lastKey = parts[parts.length - 1];\n            if (lastKey) delete target[lastKey];\n          }\n        }\n      } catch {\n        // Skip invalid JSON lines\n      }\n    }\n  }\n\n  /**\n   * Finalise a diff block: apply the unified diff to the YAML-serialized\n   * current spec, reparse, diff against current, and emit patches.\n   */\n  function finaliseDiff(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    try {\n      const currentYaml = stringify(currentSpec, { indent: 2 });\n      const patched = applyUnifiedDiff(currentYaml, diffAccumulated);\n      if (patched === false) return;\n      const newSpec = parse(patched);\n      if (newSpec && typeof newSpec === \"object\" && !Array.isArray(newSpec)) {\n        const patches = diffToPatches(\n          currentSpec,\n          newSpec as Record<string, unknown>,\n        );\n        if (patches.length > 0) {\n          currentSpec = newSpec as Record<string, unknown>;\n          emitPatches(patches, controller);\n        }\n      }\n    } catch {\n      // Diff apply or reparse failed\n    }\n  }\n\n  // ── Line processing ──\n\n  function processCompleteLine(\n    line: string,\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    const trimmed = line.trim();\n\n    // Fence open detection\n    if (fenceMode === null) {\n      if (trimmed.startsWith(YAML_SPEC_FENCE)) {\n        fenceMode = \"yaml-spec\";\n        yamlAccumulated = \"\";\n        compiler.reset(currentSpec);\n        return;\n      }\n      if (trimmed.startsWith(YAML_EDIT_FENCE)) {\n        fenceMode = \"yaml-edit\";\n        yamlAccumulated = \"\";\n        return;\n      }\n      if (trimmed.startsWith(YAML_PATCH_FENCE)) {\n        fenceMode = \"yaml-patch\";\n        yamlAccumulated = \"\";\n        return;\n      }\n      if (trimmed.startsWith(DIFF_FENCE)) {\n        fenceMode = \"diff\";\n        diffAccumulated = \"\";\n        return;\n      }\n    }\n\n    // Fence close detection\n    if (fenceMode !== null && trimmed === FENCE_CLOSE) {\n      if (fenceMode === \"yaml-spec\") {\n        finaliseYamlSpec(controller);\n      } else if (fenceMode === \"yaml-edit\") {\n        finaliseYamlEdit(controller);\n      } else if (fenceMode === \"yaml-patch\") {\n        finaliseYamlPatch(controller);\n      } else if (fenceMode === \"diff\") {\n        finaliseDiff(controller);\n      }\n      fenceMode = null;\n      return;\n    }\n\n    // Inside a fence\n    if (fenceMode === \"yaml-spec\") {\n      feedYamlSpec(line, controller);\n      return;\n    }\n    if (fenceMode === \"yaml-edit\") {\n      feedYamlEdit(line);\n      return;\n    }\n    if (fenceMode === \"yaml-patch\") {\n      yamlAccumulated += line + \"\\n\";\n      return;\n    }\n    if (fenceMode === \"diff\") {\n      diffAccumulated += line + \"\\n\";\n      return;\n    }\n\n    // Outside fence — pass through as text\n    if (!trimmed) {\n      emitTextDelta(\"\\n\", controller);\n      return;\n    }\n    emitTextDelta(line + \"\\n\", controller);\n  }\n\n  function flushBuffer(\n    controller: TransformStreamDefaultController<StreamChunk>,\n  ) {\n    if (!lineBuffer) return;\n\n    if (fenceMode !== null) {\n      processCompleteLine(lineBuffer, controller);\n    } else {\n      emitTextDelta(lineBuffer, controller);\n    }\n    lineBuffer = \"\";\n    buffering = false;\n  }\n\n  // ── TransformStream ──\n\n  return new TransformStream<StreamChunk, StreamChunk>({\n    transform(chunk, controller) {\n      switch (chunk.type) {\n        case \"text-start\": {\n          const id = (chunk as { id: string }).id;\n          const idNum = parseInt(id, 10);\n          if (!isNaN(idNum) && idNum >= textIdCounter) {\n            textIdCounter = idNum;\n          }\n          currentTextId = id;\n          inTextBlock = true;\n          controller.enqueue(chunk);\n          break;\n        }\n\n        case \"text-delta\": {\n          const delta = chunk as { id: string; delta: string };\n          const text = delta.delta;\n\n          for (let i = 0; i < text.length; i++) {\n            const ch = text.charAt(i);\n\n            if (ch === \"\\n\") {\n              if (buffering) {\n                processCompleteLine(lineBuffer, controller);\n                lineBuffer = \"\";\n                buffering = false;\n              } else if (fenceMode === null) {\n                emitTextDelta(\"\\n\", controller);\n              }\n            } else if (lineBuffer.length === 0 && !buffering) {\n              // Inside a fence, buffer everything. Outside, only buffer\n              // potential fence-open lines (start with backtick).\n              if (fenceMode !== null || ch === \"`\") {\n                buffering = true;\n                lineBuffer += ch;\n              } else {\n                emitTextDelta(ch, controller);\n              }\n            } else if (buffering) {\n              lineBuffer += ch;\n            } else {\n              emitTextDelta(ch, controller);\n            }\n          }\n          break;\n        }\n\n        case \"text-end\": {\n          flushBuffer(controller);\n          if (inTextBlock) {\n            controller.enqueue({ type: \"text-end\", id: currentTextId });\n            inTextBlock = false;\n          }\n          break;\n        }\n\n        default: {\n          controller.enqueue(chunk);\n          break;\n        }\n      }\n    },\n\n    flush(controller) {\n      flushBuffer(controller);\n      if (fenceMode === \"yaml-spec\") {\n        finaliseYamlSpec(controller);\n      } else if (fenceMode === \"yaml-edit\") {\n        finaliseYamlEdit(controller);\n      } else if (fenceMode === \"yaml-patch\") {\n        finaliseYamlPatch(controller);\n      } else if (fenceMode === \"diff\") {\n        finaliseDiff(controller);\n      }\n      closeTextBlock(controller);\n    },\n  });\n}\n\n/**\n * Convenience wrapper that pipes an AI SDK UI message stream through the\n * YAML transform, converting YAML spec/edit blocks into json-render patches.\n *\n * Drop-in replacement for `pipeJsonRender` from `@json-render/core`.\n *\n * @example\n * ```ts\n * import { pipeYamlRender } from \"@json-render/yaml\";\n *\n * const stream = createUIMessageStream({\n *   execute: async ({ writer }) => {\n *     writer.merge(pipeYamlRender(result.toUIMessageStream()));\n *   },\n * });\n * return createUIMessageStreamResponse({ stream });\n * ```\n */\nexport function pipeYamlRender<T = StreamChunk>(\n  stream: ReadableStream<T>,\n  options?: YamlTransformOptions,\n): ReadableStream<T> {\n  return stream.pipeThrough(\n    createYamlTransform(options) as unknown as TransformStream<T, T>,\n  );\n}\n"
  },
  {
    "path": "packages/yaml/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/yaml/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"@json-render/core\", \"yaml\", \"diff\"],\n});\n"
  },
  {
    "path": "packages/yaml/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    include: [\"src/**/*.test.ts\"],\n  },\n});\n"
  },
  {
    "path": "packages/zustand/CHANGELOG.md",
    "content": "# @json-render/zustand\n\n## 0.14.1\n\n### Patch Changes\n\n- Updated dependencies [43b7515]\n  - @json-render/core@0.14.1\n\n## 0.14.0\n\n### Patch Changes\n\n- Updated dependencies [a8afd8b]\n  - @json-render/core@0.14.0\n\n## 0.13.0\n\n### Patch Changes\n\n- Updated dependencies [5b32de8]\n  - @json-render/core@0.13.0\n\n## 0.12.1\n\n### Patch Changes\n\n- Updated dependencies [54a1ecf]\n  - @json-render/core@0.12.1\n\n## 0.12.0\n\n### Patch Changes\n\n- Updated dependencies [63c339b]\n  - @json-render/core@0.12.0\n\n## 0.11.0\n\n### Patch Changes\n\n- Updated dependencies [3f1e71e]\n  - @json-render/core@0.11.0\n\n## 0.10.0\n\n### Patch Changes\n\n- Updated dependencies [9cef4e9]\n  - @json-render/core@0.10.0\n\n## 0.9.1\n\n### Patch Changes\n\n- @json-render/core@0.9.1\n\n## 0.9.0\n\n### Minor Changes\n\n- 1d755c1: External state store, store adapters, and bug fixes.\n\n  ### New: External State Store\n\n  The `StateStore` interface lets you plug in your own state management (Redux, Zustand, Jotai, XState, etc.) instead of the built-in internal store. Pass a `store` prop to `StateProvider`, `JSONUIProvider`, or `createRenderer` for controlled mode.\n  - Added `StateStore` interface and `createStateStore()` factory to `@json-render/core`\n  - `StateProvider`, `JSONUIProvider`, and `createRenderer` now accept an optional `store` prop for controlled mode\n  - When `store` is provided, it becomes the single source of truth (`initialState`/`onStateChange` are ignored)\n  - When `store` is omitted, everything works exactly as before (fully backward compatible)\n  - Applied across all platform packages: react, react-native, react-pdf\n  - Store utilities (`createStoreAdapter`, `immutableSetByPath`, `flattenToPointers`) available via `@json-render/core/store-utils` for building custom adapters\n\n  ### New: Store Adapter Packages\n  - `@json-render/zustand` — Zustand adapter for `StateStore`\n  - `@json-render/redux` — Redux / Redux Toolkit adapter for `StateStore`\n  - `@json-render/jotai` — Jotai adapter for `StateStore`\n\n  ### Changed: `onStateChange` signature updated (breaking)\n\n  The `onStateChange` callback now receives a single array of changed entries instead of being called once per path:\n\n  ```ts\n  // Before\n  onStateChange?: (path: string, value: unknown) => void\n\n  // After\n  onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void\n  ```\n\n  ### Fixed\n  - Fix schema import to use server-safe `@json-render/react/schema` subpath, avoiding `createContext` crashes in Next.js App Router API routes\n  - Fix chaining actions in `@json-render/react`, `@json-render/react-native`, and `@json-render/react-pdf`\n  - Fix safely resolving inner type for Zod arrays in core schema\n\n### Patch Changes\n\n- Updated dependencies [1d755c1]\n  - @json-render/core@0.9.0\n"
  },
  {
    "path": "packages/zustand/README.md",
    "content": "# @json-render/zustand\n\nZustand adapter for json-render's `StateStore` interface. Wire a Zustand vanilla store as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/zustand @json-render/core @json-render/react zustand\n```\n\n> **Note:** This adapter requires Zustand v5+. Zustand v4 is not supported due to\n> breaking API changes in the vanilla store interface (`createStore`, `StoreApi`).\n\n## Usage\n\n```ts\nimport { createStore } from \"zustand/vanilla\";\nimport { zustandStateStore } from \"@json-render/zustand\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create a Zustand vanilla store\nconst bearStore = createStore(() => ({\n  count: 0,\n  name: \"Bear\",\n}));\n\n// 2. Create the json-render StateStore adapter\nconst store = zustandStateStore({ store: bearStore });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Zustand */}\n</StateProvider>\n```\n\n### With a nested slice\n\n```ts\nconst appStore = createStore(() => ({\n  ui: { count: 0 },\n  auth: { token: null },\n}));\n\nconst store = zustandStateStore({\n  store: appStore,\n  selector: (s) => s.ui,\n  updater: (next, s) => s.setState({ ui: next }),\n});\n```\n\n## API\n\n### `zustandStateStore(options)`\n\nCreates a `StateStore` backed by a Zustand store.\n\n#### Options\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `StoreApi<S>` | Yes | A Zustand vanilla store (from `createStore` in `zustand/vanilla`) |\n| `selector` | `(state) => StateModel` | No | Select the json-render slice from the store state. Defaults to the entire state. |\n| `updater` | `(nextState, store) => void` | No | Apply the next state back to the Zustand store. Defaults to a shallow merge so that keys outside the json-render model are preserved. Override for nested slices, or pass `(next, s) => s.setState(next, true)` for full replacement. |\n"
  },
  {
    "path": "packages/zustand/package.json",
    "content": "{\n  \"name\": \"@json-render/zustand\",\n  \"version\": \"0.14.1\",\n  \"license\": \"Apache-2.0\",\n  \"description\": \"Zustand adapter for json-render StateStore\",\n  \"keywords\": [\n    \"json-render\",\n    \"zustand\",\n    \"state-management\",\n    \"adapter\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel-labs/json-render.git\",\n    \"directory\": \"packages/zustand\"\n  },\n  \"homepage\": \"https://json-render.dev\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel-labs/json-render/issues\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/index.mjs\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup\",\n    \"dev\": \"tsup --watch\",\n    \"check-types\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@json-render/core\": \"workspace:*\"\n  },\n  \"peerDependencies\": {\n    \"zustand\": \">=5.0.0\"\n  },\n  \"devDependencies\": {\n    \"@internal/typescript-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.2\",\n    \"typescript\": \"^5.4.5\",\n    \"zustand\": \"^5.0.11\"\n  }\n}\n"
  },
  {
    "path": "packages/zustand/src/index.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { createStore } from \"zustand/vanilla\";\nimport { zustandStateStore } from \"./index\";\n\ndescribe(\"zustandStateStore\", () => {\n  it(\"get/set round-trip\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({ count: 0 }));\n    const store = zustandStateStore({ store: zStore });\n\n    expect(store.get(\"/count\")).toBe(0);\n\n    store.set(\"/count\", 42);\n\n    expect(store.get(\"/count\")).toBe(42);\n    expect(store.getSnapshot().count).toBe(42);\n  });\n\n  it(\"update round-trip with multiple values\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({}));\n    const store = zustandStateStore({ store: zStore });\n\n    store.update({ \"/a\": 1, \"/b\": \"hello\" });\n\n    expect(store.get(\"/a\")).toBe(1);\n    expect(store.get(\"/b\")).toBe(\"hello\");\n    expect(store.getSnapshot()).toEqual({ a: 1, b: \"hello\" });\n  });\n\n  it(\"subscribe fires on set\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({}));\n    const store = zustandStateStore({ store: zStore });\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"subscribe fires on update\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({}));\n    const store = zustandStateStore({ store: zStore });\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"unsubscribe stops notifications\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({}));\n    const store = zustandStateStore({ store: zStore });\n    const listener = vi.fn();\n    const unsub = store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n    expect(listener).toHaveBeenCalledTimes(1);\n\n    unsub();\n    store.set(\"/x\", 2);\n    expect(listener).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"getSnapshot immutability -- previous snapshot is not mutated\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({\n      user: { name: \"Alice\", age: 30 },\n    }));\n    const store = zustandStateStore({ store: zStore });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/user/name\", \"Bob\");\n    const snap2 = store.getSnapshot();\n\n    expect(snap1.user).toEqual({ name: \"Alice\", age: 30 });\n    expect((snap2.user as Record<string, unknown>).name).toBe(\"Bob\");\n    expect(snap1.user).not.toBe(snap2.user);\n  });\n\n  it(\"structural sharing -- untouched branches keep references\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({\n      a: { x: 1 },\n      b: { y: 2 },\n    }));\n    const store = zustandStateStore({ store: zStore });\n    const snap1 = store.getSnapshot();\n\n    store.set(\"/a/x\", 99);\n    const snap2 = store.getSnapshot();\n\n    expect(snap2.b).toBe(snap1.b);\n    expect(snap2.a).not.toBe(snap1.a);\n  });\n\n  it(\"getServerSnapshot returns same as getSnapshot\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({ x: 1 }));\n    const store = zustandStateStore({ store: zStore });\n\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n\n    store.set(\"/x\", 2);\n    expect(store.getServerSnapshot!()).toBe(store.getSnapshot());\n  });\n\n  it(\"set skips update when value is unchanged\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({ x: 1 }));\n    const store = zustandStateStore({ store: zStore });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.set(\"/x\", 1);\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"update skips update when no values changed\", () => {\n    const zStore = createStore<Record<string, unknown>>()(() => ({\n      a: 1,\n      b: 2,\n    }));\n    const store = zustandStateStore({ store: zStore });\n    const snap1 = store.getSnapshot();\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    store.update({ \"/a\": 1, \"/b\": 2 });\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.getSnapshot()).toBe(snap1);\n  });\n\n  it(\"subscribe does NOT fire when unrelated slice changes\", () => {\n    interface AppState extends Record<string, unknown> {\n      ui: Record<string, unknown>;\n      other: { value: string };\n    }\n\n    const zStore = createStore<AppState>()(() => ({\n      ui: { count: 0 },\n      other: { value: \"a\" },\n    }));\n\n    const store = zustandStateStore({\n      store: zStore,\n      selector: (s) => s.ui,\n      updater: (next, s) => s.setState({ ui: next }),\n    });\n\n    const listener = vi.fn();\n    store.subscribe(listener);\n\n    zStore.setState({ other: { value: \"b\" } });\n\n    expect(listener).not.toHaveBeenCalled();\n    expect(store.get(\"/count\")).toBe(0);\n  });\n\n  it(\"default updater preserves keys outside the json-render model\", () => {\n    interface AppState extends Record<string, unknown> {\n      count: number;\n      theme: string;\n    }\n\n    const zStore = createStore<AppState>()(() => ({\n      count: 0,\n      theme: \"dark\",\n    }));\n    const store = zustandStateStore({ store: zStore });\n\n    store.set(\"/count\", 5);\n\n    expect(store.get(\"/count\")).toBe(5);\n    expect(zStore.getState().theme).toBe(\"dark\");\n  });\n\n  it(\"works with custom selector and updater\", () => {\n    interface AppState extends Record<string, unknown> {\n      ui: Record<string, unknown>;\n    }\n\n    const zStore = createStore<AppState>()(() => ({\n      ui: { count: 0 },\n    }));\n\n    const store = zustandStateStore({\n      store: zStore,\n      selector: (s) => s.ui,\n      updater: (next, s) => s.setState({ ui: next }),\n    });\n\n    store.set(\"/count\", 5);\n\n    expect(store.get(\"/count\")).toBe(5);\n    expect(zStore.getState().ui.count).toBe(5);\n  });\n});\n"
  },
  {
    "path": "packages/zustand/src/index.ts",
    "content": "import type { StateModel, StateStore } from \"@json-render/core\";\nimport { createStoreAdapter } from \"@json-render/core/store-utils\";\nimport type { StoreApi } from \"zustand\";\n\nexport type { StateStore } from \"@json-render/core\";\n\n/**\n * Options for {@link zustandStateStore}.\n */\nexport interface ZustandStateStoreOptions<S extends StateModel = StateModel> {\n  /** A Zustand vanilla store (created with `createStore` from `zustand/vanilla`). */\n  store: StoreApi<S>;\n  /**\n   * Select the json-render state slice from the Zustand store state.\n   * Defaults to `(state) => state` (the entire store is the state model).\n   */\n  selector?: (state: S) => StateModel;\n  /**\n   * Apply a state change back to the Zustand store.\n   * Defaults to a shallow merge (`store.setState(next)`) so that keys\n   * outside the json-render model are preserved. Override this if you use\n   * a selector and only want to update a nested slice, or pass\n   * `(next, s) => s.setState(next as S, true)` for full replacement.\n   */\n  updater?: (nextState: StateModel, store: StoreApi<S>) => void;\n}\n\n/**\n * Create a {@link StateStore} backed by a Zustand store.\n *\n * @example\n * ```ts\n * import { createStore } from \"zustand/vanilla\";\n * import { zustandStateStore } from \"@json-render/zustand\";\n *\n * const bearStore = createStore(() => ({ count: 0, name: \"Bear\" }));\n *\n * const store = zustandStateStore({ store: bearStore });\n *\n * <StateProvider store={store}>...</StateProvider>\n * ```\n *\n * @example Using a selector for a nested slice:\n * ```ts\n * const appStore = createStore(() => ({\n *   ui: { count: 0 },\n *   auth: { token: null },\n * }));\n *\n * const store = zustandStateStore({\n *   store: appStore,\n *   selector: (s) => s.ui,\n *   updater: (next, s) => s.setState({ ui: next }),\n * });\n * ```\n */\nexport function zustandStateStore<S extends StateModel = StateModel>(\n  options: ZustandStateStoreOptions<S>,\n): StateStore {\n  const {\n    store,\n    selector = (s: S) => s as StateModel,\n    updater = (next, s) => s.setState(next as Partial<S>),\n  } = options;\n\n  return createStoreAdapter({\n    getSnapshot: () => selector(store.getState()),\n    setSnapshot: (next) => updater(next, store),\n    subscribe(listener) {\n      let prev = selector(store.getState());\n      return store.subscribe(() => {\n        const current = selector(store.getState());\n        if (current !== prev) {\n          prev = current;\n          listener();\n          // Re-read after listener in case it triggered a synchronous dispatch;\n          // absorb that change so it doesn't fire a duplicate notification.\n          prev = selector(store.getState());\n        }\n      });\n    },\n  });\n}\n"
  },
  {
    "path": "packages/zustand/tsconfig.json",
    "content": "{\n  \"extends\": \"@internal/typescript-config/base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\"\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/zustand/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: [\"src/index.ts\"],\n  format: [\"cjs\", \"esm\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  external: [\"@json-render/core\", \"@json-render/core/store-utils\", \"zustand\"],\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"apps/*\"\n  - \"examples/*\"\n  - \"examples/stripe-app/*\"\n  - \"packages/*\"\n  - \"tests/*\"\n"
  },
  {
    "path": "scripts/generate-og-images.mts",
    "content": "import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport satori from \"satori\";\nimport { Resvg } from \"@resvg/resvg-js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst ROOT = join(__dirname, \"..\");\nconst FONTS_DIR = join(ROOT, \"apps/web/public\");\n\nconst WIDTH = 1200;\nconst HEIGHT = 630;\n\ntype Framework = \"nextjs\" | \"vite\" | \"sveltekit\";\n\ninterface Example {\n  dir: string;\n  title: string;\n  framework: Framework;\n}\n\nconst EXAMPLES: Example[] = [\n  { dir: \"chat\", title: \"Chat Example\", framework: \"nextjs\" },\n  { dir: \"dashboard\", title: \"Dashboard Example\", framework: \"nextjs\" },\n  { dir: \"image\", title: \"Image Example\", framework: \"nextjs\" },\n  { dir: \"no-ai\", title: \"No AI Example\", framework: \"nextjs\" },\n  { dir: \"react-email\", title: \"React Email Example\", framework: \"nextjs\" },\n  { dir: \"react-pdf\", title: \"React PDF Example\", framework: \"nextjs\" },\n  { dir: \"react-three-fiber\", title: \"React Three Fiber Example\", framework: \"nextjs\" },\n  { dir: \"remotion\", title: \"Remotion Example\", framework: \"nextjs\" },\n  { dir: \"solid\", title: \"Solid Example\", framework: \"vite\" },\n  { dir: \"svelte\", title: \"Svelte Example\", framework: \"vite\" },\n  { dir: \"svelte-chat\", title: \"Svelte Chat Example\", framework: \"sveltekit\" },\n  { dir: \"vue\", title: \"Vue Example\", framework: \"vite\" },\n  { dir: \"vite-renderers\", title: \"Multi-Renderer Example\", framework: \"vite\" },\n];\n\nfunction getOutputPath(example: Example): string {\n  const base = join(ROOT, \"examples\", example.dir);\n  switch (example.framework) {\n    case \"nextjs\":\n      return join(base, \"app\", \"opengraph-image.png\");\n    case \"vite\":\n      return join(base, \"public\", \"og-image.png\");\n    case \"sveltekit\":\n      return join(base, \"static\", \"og-image.png\");\n  }\n}\n\nfunction buildMarkup(title: string) {\n  return {\n    type: \"div\",\n    props: {\n      style: {\n        width: \"100%\",\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        backgroundColor: \"black\",\n        padding: \"60px 80px\",\n      },\n      children: [\n        {\n          type: \"div\",\n          props: {\n            style: {\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: \"16px\",\n            },\n            children: [\n              {\n                type: \"svg\",\n                props: {\n                  width: 36,\n                  height: 36,\n                  viewBox: \"0 0 16 16\",\n                  fill: \"white\",\n                  children: {\n                    type: \"path\",\n                    props: {\n                      fillRule: \"evenodd\",\n                      clipRule: \"evenodd\",\n                      d: \"M8 1L16 15H0L8 1Z\",\n                    },\n                  },\n                },\n              },\n              {\n                type: \"span\",\n                props: {\n                  style: {\n                    fontSize: 36,\n                    color: \"#666\",\n                    fontFamily: \"Geist\",\n                    fontWeight: 400,\n                  },\n                  children: \"/\",\n                },\n              },\n              {\n                type: \"span\",\n                props: {\n                  style: {\n                    fontSize: 36,\n                    fontFamily: \"Geist Pixel Square\",\n                    fontWeight: 500,\n                    color: \"white\",\n                  },\n                  children: \"json-render\",\n                },\n              },\n            ],\n          },\n        },\n        {\n          type: \"div\",\n          props: {\n            style: {\n              display: \"flex\",\n              flex: 1,\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n            },\n            children: title.split(\"\\n\").map((line: string) => ({\n              type: \"span\",\n              props: {\n                style: {\n                  fontSize: 72,\n                  fontFamily: \"Geist\",\n                  fontWeight: 400,\n                  color: \"white\",\n                  letterSpacing: \"-0.02em\",\n                  textAlign: \"center\",\n                  lineHeight: 1.2,\n                },\n                children: line,\n              },\n            })),\n          },\n        },\n      ],\n    },\n  };\n}\n\nasync function main() {\n  const [geistRegular, geistPixelSquare] = await Promise.all([\n    readFile(join(FONTS_DIR, \"Geist-Regular.ttf\")),\n    readFile(join(FONTS_DIR, \"GeistPixel-Square.ttf\")),\n  ]);\n\n  const fonts = [\n    { name: \"Geist\", data: geistRegular, weight: 400 as const, style: \"normal\" as const },\n    { name: \"Geist Pixel Square\", data: geistPixelSquare, weight: 500 as const, style: \"normal\" as const },\n  ];\n\n  for (const example of EXAMPLES) {\n    const svg = await satori(buildMarkup(example.title), {\n      width: WIDTH,\n      height: HEIGHT,\n      fonts,\n    });\n\n    const resvg = new Resvg(svg, {\n      fitTo: { mode: \"width\", value: WIDTH },\n    });\n    const png = resvg.render().asPng();\n\n    const outPath = getOutputPath(example);\n    await mkdir(dirname(outPath), { recursive: true });\n    await writeFile(outPath, png);\n\n    console.log(`  ${example.dir} -> ${outPath.replace(ROOT + \"/\", \"\")}`);\n  }\n\n  console.log(`\\nGenerated ${EXAMPLES.length} OG images.`);\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "skills/codegen/SKILL.md",
    "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"
  },
  {
    "path": "skills/core/SKILL.md",
    "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## 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## 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);\nconst fixed = autoFixSpec(spec);\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 |\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"
  },
  {
    "path": "skills/image/SKILL.md",
    "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"
  },
  {
    "path": "skills/jotai/SKILL.md",
    "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"
  },
  {
    "path": "skills/mcp/SKILL.md",
    "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"
  },
  {
    "path": "skills/react/SKILL.md",
    "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      description: \"Card container with title\",\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  },\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\": [\n      { \"type\": \"Button\", \"props\": { \"label\": \"Click me\" } }\n    ]\n  }\n}\n```\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- **`{ \"$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\": { \"value\": { \"$bindState\": \"/form/country\" }, \"options\": [\"US\", \"Canada\"] },\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"
  },
  {
    "path": "skills/react-email/SKILL.md",
    "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`), 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"
  },
  {
    "path": "skills/react-native/SKILL.md",
    "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- **`{ \"$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"
  },
  {
    "path": "skills/react-pdf/SKILL.md",
    "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\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"
  },
  {
    "path": "skills/react-three-fiber/SKILL.md",
    "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"
  },
  {
    "path": "skills/redux/SKILL.md",
    "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"
  },
  {
    "path": "skills/remotion/SKILL.md",
    "content": "---\nname: remotion\ndescription: 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.\n---\n\n# @json-render/remotion\n\nRemotion renderer that converts JSON timeline specs into video compositions.\n\n## Quick Start\n\n```typescript\nimport { Player } from \"@remotion/player\";\nimport { Renderer, type TimelineSpec } from \"@json-render/remotion\";\n\nfunction VideoPlayer({ spec }: { spec: TimelineSpec }) {\n  return (\n    <Player\n      component={Renderer}\n      inputProps={{ spec }}\n      durationInFrames={spec.composition.durationInFrames}\n      fps={spec.composition.fps}\n      compositionWidth={spec.composition.width}\n      compositionHeight={spec.composition.height}\n      controls\n    />\n  );\n}\n```\n\n## Using Standard Components\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport {\n  schema,\n  standardComponentDefinitions,\n  standardTransitionDefinitions,\n  standardEffectDefinitions,\n} from \"@json-render/remotion\";\n\nexport const videoCatalog = defineCatalog(schema, {\n  components: standardComponentDefinitions,\n  transitions: standardTransitionDefinitions,\n  effects: standardEffectDefinitions,\n});\n```\n\n## Adding Custom Components\n\n```typescript\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    ...standardComponentDefinitions,\n    MyCustomClip: {\n      props: z.object({ text: z.string() }),\n      type: \"scene\",\n      defaultDuration: 90,\n      description: \"My custom video clip\",\n    },\n  },\n});\n\n// Pass custom component to Renderer\n<Player\n  component={Renderer}\n  inputProps={{\n    spec,\n    components: { MyCustomClip: MyCustomComponent },\n  }}\n/>\n```\n\n## Timeline Spec Structure\n\n```json\n{\n  \"composition\": { \"id\": \"video\", \"fps\": 30, \"width\": 1920, \"height\": 1080, \"durationInFrames\": 300 },\n  \"tracks\": [{ \"id\": \"main\", \"name\": \"Main\", \"type\": \"video\", \"enabled\": true }],\n  \"clips\": [\n    { \"id\": \"clip-1\", \"trackId\": \"main\", \"component\": \"TitleCard\", \"props\": { \"title\": \"Hello\" }, \"from\": 0, \"durationInFrames\": 90 }\n  ],\n  \"audio\": { \"tracks\": [] }\n}\n```\n\n## Standard Components\n\n| Component | Type | Description |\n|-----------|------|-------------|\n| `TitleCard` | scene | Full-screen title with subtitle |\n| `TypingText` | scene | Terminal-style typing animation |\n| `ImageSlide` | image | Full-screen image display |\n| `SplitScreen` | scene | Two-panel comparison |\n| `QuoteCard` | scene | Quote with attribution |\n| `StatCard` | scene | Animated statistic display |\n| `TextOverlay` | overlay | Text overlay |\n| `LowerThird` | overlay | Name/title overlay |\n\n## Key Exports\n\n| Export | Purpose |\n|--------|---------|\n| `Renderer` | Render spec to Remotion composition |\n| `schema` | Timeline schema |\n| `standardComponents` | Pre-built component registry |\n| `standardComponentDefinitions` | Catalog definitions |\n| `useTransition` | Transition animation hook |\n| `ClipWrapper` | Wrap clips with transitions |\n"
  },
  {
    "path": "skills/remotion-best-practices/SKILL.md",
    "content": "---\nname: remotion-best-practices\ndescription: Best practices for Remotion - Video creation in React\nmetadata:\n  tags: remotion, video, react, animation, composition\n---\n\n## When to use\n\nUse this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.\n\n## Captions\n\nWhen dealing with captions or subtitles, load the [./rules/subtitles.md](./rules/subtitles.md) file for more information.\n\n## How to use\n\nRead individual rule files for detailed explanations and code examples:\n\n- [rules/3d.md](rules/3d.md) - 3D content in Remotion using Three.js and React Three Fiber\n- [rules/animations.md](rules/animations.md) - Fundamental animation skills for Remotion\n- [rules/assets.md](rules/assets.md) - Importing images, videos, audio, and fonts into Remotion\n- [rules/audio.md](rules/audio.md) - Using audio and sound in Remotion - importing, trimming, volume, speed, pitch\n- [rules/calculate-metadata.md](rules/calculate-metadata.md) - Dynamically set composition duration, dimensions, and props\n- [rules/can-decode.md](rules/can-decode.md) - Check if a video can be decoded by the browser using Mediabunny\n- [rules/charts.md](rules/charts.md) - Chart and data visualization patterns for Remotion\n- [rules/compositions.md](rules/compositions.md) - Defining compositions, stills, folders, default props and dynamic metadata\n- [rules/extract-frames.md](rules/extract-frames.md) - Extract frames from videos at specific timestamps using Mediabunny\n- [rules/fonts.md](rules/fonts.md) - Loading Google Fonts and local fonts in Remotion\n- [rules/get-audio-duration.md](rules/get-audio-duration.md) - Getting the duration of an audio file in seconds with Mediabunny\n- [rules/get-video-dimensions.md](rules/get-video-dimensions.md) - Getting the width and height of a video file with Mediabunny\n- [rules/get-video-duration.md](rules/get-video-duration.md) - Getting the duration of a video file in seconds with Mediabunny\n- [rules/gifs.md](rules/gifs.md) - Displaying GIFs synchronized with Remotion's timeline\n- [rules/images.md](rules/images.md) - Embedding images in Remotion using the Img component\n- [rules/light-leaks.md](rules/light-leaks.md) - Light leak overlay effects using @remotion/light-leaks\n- [rules/lottie.md](rules/lottie.md) - Embedding Lottie animations in Remotion\n- [rules/measuring-dom-nodes.md](rules/measuring-dom-nodes.md) - Measuring DOM element dimensions in Remotion\n- [rules/measuring-text.md](rules/measuring-text.md) - Measuring text dimensions, fitting text to containers, and checking overflow\n- [rules/sequencing.md](rules/sequencing.md) - Sequencing patterns for Remotion - delay, trim, limit duration of items\n- [rules/tailwind.md](rules/tailwind.md) - Using TailwindCSS in Remotion\n- [rules/text-animations.md](rules/text-animations.md) - Typography and text animation patterns for Remotion\n- [rules/timing.md](rules/timing.md) - Interpolation curves in Remotion - linear, easing, spring animations\n- [rules/transitions.md](rules/transitions.md) - Scene transition patterns for Remotion\n- [rules/transparent-videos.md](rules/transparent-videos.md) - Rendering out a video with transparency\n- [rules/trimming.md](rules/trimming.md) - Trimming patterns for Remotion - cut the beginning or end of animations\n- [rules/videos.md](rules/videos.md) - Embedding videos in Remotion - trimming, volume, speed, looping, pitch\n- [rules/parameters.md](rules/parameters.md) - Make a video parametrizable by adding a Zod schema\n- [rules/maps.md](rules/maps.md) - Add a map using Mapbox and animate it\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/3d.md",
    "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```"
  },
  {
    "path": "skills/remotion-best-practices/rules/animations.md",
    "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.  "
  },
  {
    "path": "skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx",
    "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport {\n  AbsoluteFill,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily } = loadFont();\n\nconst COLOR_BAR = \"#D4AF37\";\nconst COLOR_TEXT = \"#ffffff\";\nconst COLOR_MUTED = \"#888888\";\nconst COLOR_BG = \"#0a0a0a\";\nconst COLOR_AXIS = \"#333333\";\n\n// Ideal composition size: 1280x720\n\nconst Title: React.FC<{ children: React.ReactNode }> = ({ children }) => (\n  <div style={{ textAlign: \"center\", marginBottom: 40 }}>\n    <div style={{ color: COLOR_TEXT, fontSize: 48, fontWeight: 600 }}>\n      {children}\n    </div>\n  </div>\n);\n\nconst YAxis: React.FC<{ steps: number[]; height: number }> = ({\n  steps,\n  height,\n}) => (\n  <div\n    style={{\n      display: \"flex\",\n      flexDirection: \"column\",\n      justifyContent: \"space-between\",\n      height,\n      paddingRight: 16,\n    }}\n  >\n    {steps\n      .slice()\n      .reverse()\n      .map((step) => (\n        <div\n          key={step}\n          style={{\n            color: COLOR_MUTED,\n            fontSize: 20,\n            textAlign: \"right\",\n          }}\n        >\n          {step.toLocaleString()}\n        </div>\n      ))}\n  </div>\n);\n\nconst Bar: React.FC<{\n  height: number;\n  progress: number;\n}> = ({ height, progress }) => (\n  <div\n    style={{\n      flex: 1,\n      display: \"flex\",\n      flexDirection: \"column\",\n      justifyContent: \"flex-end\",\n    }}\n  >\n    <div\n      style={{\n        width: \"100%\",\n        height,\n        backgroundColor: COLOR_BAR,\n        borderRadius: \"8px 8px 0 0\",\n        opacity: progress,\n      }}\n    />\n  </div>\n);\n\nconst XAxis: React.FC<{\n  children: React.ReactNode;\n  labels: string[];\n  height: number;\n}> = ({ children, labels, height }) => (\n  <div style={{ flex: 1, display: \"flex\", flexDirection: \"column\" }}>\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-end\",\n        gap: 16,\n        height,\n        borderLeft: `2px solid ${COLOR_AXIS}`,\n        borderBottom: `2px solid ${COLOR_AXIS}`,\n        paddingLeft: 16,\n      }}\n    >\n      {children}\n    </div>\n    <div\n      style={{\n        display: \"flex\",\n        gap: 16,\n        paddingLeft: 16,\n        marginTop: 12,\n      }}\n    >\n      {labels.map((label) => (\n        <div\n          key={label}\n          style={{\n            flex: 1,\n            textAlign: \"center\",\n            color: COLOR_MUTED,\n            fontSize: 20,\n          }}\n        >\n          {label}\n        </div>\n      ))}\n    </div>\n  </div>\n);\n\nexport const MyAnimation = () => {\n  const frame = useCurrentFrame();\n  const { fps, height } = useVideoConfig();\n\n  const data = [\n    { month: \"Jan\", price: 2039 },\n    { month: \"Mar\", price: 2160 },\n    { month: \"May\", price: 2327 },\n    { month: \"Jul\", price: 2426 },\n    { month: \"Sep\", price: 2634 },\n    { month: \"Nov\", price: 2672 },\n  ];\n\n  const minPrice = 2000;\n  const maxPrice = 2800;\n  const priceRange = maxPrice - minPrice;\n  const chartHeight = height - 280;\n  const yAxisSteps = [2000, 2400, 2800];\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: COLOR_BG,\n        padding: 60,\n        display: \"flex\",\n        flexDirection: \"column\",\n        fontFamily,\n      }}\n    >\n      <Title>Gold Price 2024</Title>\n\n      <div style={{ display: \"flex\", flex: 1 }}>\n        <YAxis steps={yAxisSteps} height={chartHeight} />\n        <XAxis height={chartHeight} labels={data.map((d) => d.month)}>\n          {data.map((item, i) => {\n            const progress = spring({\n              frame: frame - i * 5 - 10,\n              fps,\n              config: { damping: 18, stiffness: 80 },\n            });\n\n            const barHeight =\n              ((item.price - minPrice) / priceRange) * chartHeight * progress;\n\n            return (\n              <Bar key={item.month} height={barHeight} progress={progress} />\n            );\n          })}\n        </XAxis>\n      </div>\n    </AbsoluteFill>\n  );\n};\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx",
    "content": "import {\n  AbsoluteFill,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst COLOR_BG = \"#ffffff\";\nconst COLOR_TEXT = \"#000000\";\nconst FULL_TEXT = \"From prompt to motion graphics. This is Remotion.\";\nconst PAUSE_AFTER = \"From prompt to motion graphics.\";\nconst FONT_SIZE = 72;\nconst FONT_WEIGHT = 700;\nconst CHAR_FRAMES = 2;\nconst CURSOR_BLINK_FRAMES = 16;\nconst PAUSE_SECONDS = 1;\n\n// Ideal composition size: 1280x720\n\nconst getTypedText = ({\n  frame,\n  fullText,\n  pauseAfter,\n  charFrames,\n  pauseFrames,\n}: {\n  frame: number;\n  fullText: string;\n  pauseAfter: string;\n  charFrames: number;\n  pauseFrames: number;\n}): string => {\n  const pauseIndex = fullText.indexOf(pauseAfter);\n  const preLen =\n    pauseIndex >= 0 ? pauseIndex + pauseAfter.length : fullText.length;\n\n  let typedChars = 0;\n  if (frame < preLen * charFrames) {\n    typedChars = Math.floor(frame / charFrames);\n  } else if (frame < preLen * charFrames + pauseFrames) {\n    typedChars = preLen;\n  } else {\n    const postPhase = frame - preLen * charFrames - pauseFrames;\n    typedChars = Math.min(\n      fullText.length,\n      preLen + Math.floor(postPhase / charFrames),\n    );\n  }\n  return fullText.slice(0, typedChars);\n};\n\nconst Cursor: React.FC<{\n  frame: number;\n  blinkFrames: number;\n  symbol?: string;\n}> = ({ frame, blinkFrames, symbol = \"\\u258C\" }) => {\n  const opacity = interpolate(\n    frame % blinkFrames,\n    [0, blinkFrames / 2, blinkFrames],\n    [1, 0, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n\n  return <span style={{ opacity }}>{symbol}</span>;\n};\n\nexport const MyAnimation = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const pauseFrames = Math.round(fps * PAUSE_SECONDS);\n\n  const typedText = getTypedText({\n    frame,\n    fullText: FULL_TEXT,\n    pauseAfter: PAUSE_AFTER,\n    charFrames: CHAR_FRAMES,\n    pauseFrames,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: COLOR_BG,\n      }}\n    >\n      <div\n        style={{\n          color: COLOR_TEXT,\n          fontSize: FONT_SIZE,\n          fontWeight: FONT_WEIGHT,\n          fontFamily: \"sans-serif\",\n        }}\n      >\n        <span>{typedText}</span>\n        <Cursor frame={frame} blinkFrames={CURSOR_BLINK_FRAMES} />\n      </div>\n    </AbsoluteFill>\n  );\n};\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx",
    "content": "import { loadFont } from \"@remotion/google-fonts/Inter\";\nimport React from \"react\";\nimport {\n  AbsoluteFill,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\n/*\n * Highlight a word in a sentence with a spring-animated wipe effect.\n */\n\n// Ideal composition size: 1280x720\n\nconst COLOR_BG = \"#ffffff\";\nconst COLOR_TEXT = \"#000000\";\nconst COLOR_HIGHLIGHT = \"#A7C7E7\";\nconst FULL_TEXT = \"This is Remotion.\";\nconst HIGHLIGHT_WORD = \"Remotion\";\nconst FONT_SIZE = 72;\nconst FONT_WEIGHT = 700;\nconst HIGHLIGHT_START_FRAME = 30;\nconst HIGHLIGHT_WIPE_DURATION = 18;\n\nconst { fontFamily } = loadFont();\n\nconst Highlight: React.FC<{\n  word: string;\n  color: string;\n  delay: number;\n  durationInFrames: number;\n}> = ({ word, color, delay, durationInFrames }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const highlightProgress = spring({\n    fps,\n    frame,\n    config: { damping: 200 },\n    delay,\n    durationInFrames,\n  });\n  const scaleX = Math.max(0, Math.min(1, highlightProgress));\n\n  return (\n    <span style={{ position: \"relative\", display: \"inline-block\" }}>\n      <span\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: \"50%\",\n          height: \"1.05em\",\n          transform: `translateY(-50%) scaleX(${scaleX})`,\n          transformOrigin: \"left center\",\n          backgroundColor: color,\n          borderRadius: \"0.18em\",\n          zIndex: 0,\n        }}\n      />\n      <span style={{ position: \"relative\", zIndex: 1 }}>{word}</span>\n    </span>\n  );\n};\n\nexport const MyAnimation = () => {\n  const highlightIndex = FULL_TEXT.indexOf(HIGHLIGHT_WORD);\n  const hasHighlight = highlightIndex >= 0;\n  const preText = hasHighlight ? FULL_TEXT.slice(0, highlightIndex) : FULL_TEXT;\n  const postText = hasHighlight\n    ? FULL_TEXT.slice(highlightIndex + HIGHLIGHT_WORD.length)\n    : \"\";\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: COLOR_BG,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontFamily,\n      }}\n    >\n      <div\n        style={{\n          color: COLOR_TEXT,\n          fontSize: FONT_SIZE,\n          fontWeight: FONT_WEIGHT,\n        }}\n      >\n        {hasHighlight ? (\n          <>\n            <span>{preText}</span>\n            <Highlight\n              word={HIGHLIGHT_WORD}\n              color={COLOR_HIGHLIGHT}\n              delay={HIGHLIGHT_START_FRAME}\n              durationInFrames={HIGHLIGHT_WIPE_DURATION}\n            />\n            <span>{postText}</span>\n          </>\n        ) : (\n          <span>{FULL_TEXT}</span>\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n};\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/assets.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/audio.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/calculate-metadata.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/can-decode.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/charts.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/compositions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/display-captions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/extract-frames.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/fonts.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/get-audio-duration.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/get-video-dimensions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/get-video-duration.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/gifs.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/images.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/import-srt-captions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/light-leaks.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/lottie.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/maps.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/measuring-dom-nodes.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/measuring-text.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/parameters.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/sequencing.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/subtitles.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/tailwind.md",
    "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."
  },
  {
    "path": "skills/remotion-best-practices/rules/text-animations.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/timing.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/transcribe-captions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/transitions.md",
    "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"
  },
  {
    "path": "skills/remotion-best-practices/rules/transparent-videos.md",
    "content": "---\nname: transparent-videos\ndescription: Rendering transparent videos in Remotion\nmetadata:\n  tags: transparent, alpha, codec, vp9, prores, webm\n---\n\n# Rendering Transparent Videos\n\nRemotion can render transparent videos in two ways: as a ProRes video or as a WebM video.\n\n## Transparent ProRes\n\nIdeal for when importing into video editing software.\n\n**CLI:**\n\n```bash\nnpx remotion render --image-format=png --pixel-format=yuva444p10le --codec=prores --prores-profile=4444 MyComp out.mov\n```\n\n**Default in Studio** (restart Studio after changing):\n\n```ts\n// remotion.config.ts\nimport { Config } from \"@remotion/cli/config\";\n\nConfig.setVideoImageFormat(\"png\");\nConfig.setPixelFormat(\"yuva444p10le\");\nConfig.setCodec(\"prores\");\nConfig.setProResProfile(\"4444\");\n```\n\n**Setting it as the default export settings for a composition** (using `calculateMetadata`):\n\n```tsx\nimport { CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({\n  props,\n}) => {\n  return {\n    defaultCodec: \"prores\",\n    defaultVideoImageFormat: \"png\",\n    defaultPixelFormat: \"yuva444p10le\",\n    defaultProResProfile: \"4444\",\n  };\n};\n\n<Composition\n  id=\"my-video\"\n  component={MyVideo}\n  durationInFrames={150}\n  fps={30}\n  width={1920}\n  height={1080}\n  calculateMetadata={calculateMetadata}\n/>;\n```\n\n## Transparent WebM (VP9)\n\nIdeal for when playing in a browser.\n\n**CLI:**\n\n```bash\nnpx remotion render --image-format=png --pixel-format=yuva420p --codec=vp9 MyComp out.webm\n```\n\n**Default in Studio** (restart Studio after changing):\n\n```ts\n// remotion.config.ts\nimport { Config } from \"@remotion/cli/config\";\n\nConfig.setVideoImageFormat(\"png\");\nConfig.setPixelFormat(\"yuva420p\");\nConfig.setCodec(\"vp9\");\n```\n\n**Setting it as the default export settings for a composition** (using `calculateMetadata`):\n\n```tsx\nimport { CalculateMetadataFunction } from \"remotion\";\n\nconst calculateMetadata: CalculateMetadataFunction<Props> = async ({\n  props,\n}) => {\n  return {\n    defaultCodec: \"vp8\",\n    defaultVideoImageFormat: \"png\",\n    defaultPixelFormat: \"yuva420p\",\n  };\n};\n\n<Composition\n  id=\"my-video\"\n  component={MyVideo}\n  durationInFrames={150}\n  fps={30}\n  width={1920}\n  height={1080}\n  calculateMetadata={calculateMetadata}\n/>;\n```\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/trimming.md",
    "content": "---\nname: trimming\ndescription: Trimming patterns for Remotion - cut the beginning or end of animations\nmetadata:\n  tags: sequence, trim, clip, cut, offset\n---\n\nUse `<Sequence>` with a negative `from` value to trim the start of an animation.\n\n## Trim the Beginning\n\nA negative `from` value shifts time backwards, making the animation start partway through:\n\n```tsx\nimport { Sequence, useVideoConfig } from \"remotion\";\n\nconst fps = useVideoConfig();\n\n<Sequence from={-0.5 * fps}>\n  <MyAnimation />\n</Sequence>\n```\n\nThe animation appears 15 frames into its progress - the first 15 frames are trimmed off.\nInside `<MyAnimation>`, `useCurrentFrame()` starts at 15 instead of 0.\n\n## Trim the End\n\nUse `durationInFrames` to unmount content after a specified duration:\n\n```tsx\n\n<Sequence durationInFrames={1.5 * fps}>\n  <MyAnimation />\n</Sequence>\n```\n\nThe animation plays for 45 frames, then the component unmounts.\n\n## Trim and Delay\n\nNest sequences to both trim the beginning and delay when it appears:\n\n```tsx\n<Sequence from={30}>\n  <Sequence from={-15}>\n    <MyAnimation />\n  </Sequence>\n</Sequence>\n```\n\nThe inner sequence trims 15 frames from the start, and the outer sequence delays the result by 30 frames.\n\n"
  },
  {
    "path": "skills/remotion-best-practices/rules/videos.md",
    "content": "---\nname: videos\ndescription: Embedding videos in Remotion - trimming, volume, speed, looping, pitch\nmetadata:\n  tags: video, media, trim, volume, speed, loop, pitch\n---\n\n# Using videos in Remotion\n\n## Prerequisites\n\nFirst, the @remotion/media package needs to be installed.  \nIf it is not, use the following command:\n\n```bash\nnpx remotion add @remotion/media # If project uses npm\nbunx remotion add @remotion/media # If project uses bun\nyarn remotion add @remotion/media # If project uses yarn\npnpm exec remotion add @remotion/media # If project uses pnpm\n```\n\nUse `<Video>` from `@remotion/media` to embed videos into your composition.\n\n```tsx\nimport { Video } from \"@remotion/media\";\nimport { staticFile } from \"remotion\";\n\nexport const MyComposition = () => {\n  return <Video src={staticFile(\"video.mp4\")} />;\n};\n```\n\nRemote URLs are also supported:\n\n```tsx\n<Video src=\"https://remotion.media/video.mp4\" />\n```\n\n## Trimming\n\nUse `trimBefore` and `trimAfter` to remove portions of the video. Values are in seconds.\n\n```tsx\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Video\n    src={staticFile(\"video.mp4\")}\n    trimBefore={2 * fps} // Skip the first 2 seconds\n    trimAfter={10 * fps} // End at the 10 second mark\n  />\n);\n```\n\n## Delaying\n\nWrap the video in a `<Sequence>` to delay when it appears:\n\n```tsx\nimport { Sequence, staticFile } from \"remotion\";\nimport { Video } from \"@remotion/media\";\n\nconst { fps } = useVideoConfig();\n\nreturn (\n  <Sequence from={1 * fps}>\n    <Video src={staticFile(\"video.mp4\")} />\n  </Sequence>\n);\n```\n\nThe video will appear after 1 second.\n\n## Sizing and Position\n\nUse the `style` prop to control size and position:\n\n```tsx\n<Video\n  src={staticFile(\"video.mp4\")}\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## Volume\n\nSet a static volume (0 to 1):\n\n```tsx\n<Video src={staticFile(\"video.mp4\")} 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  <Video\n    src={staticFile(\"video.mp4\")}\n    volume={(f) =>\n      interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: \"clamp\" })\n    }\n  />\n);\n```\n\nUse `muted` to silence the video entirely:\n\n```tsx\n<Video src={staticFile(\"video.mp4\")} muted />\n```\n\n## Speed\n\nUse `playbackRate` to change the playback speed:\n\n```tsx\n<Video src={staticFile(\"video.mp4\")} playbackRate={2} /> {/* 2x speed */}\n<Video src={staticFile(\"video.mp4\")} playbackRate={0.5} /> {/* Half speed */}\n```\n\nReverse playback is not supported.\n\n## Looping\n\nUse `loop` to loop the video indefinitely:\n\n```tsx\n<Video src={staticFile(\"video.mp4\")} loop />\n```\n\nUse `loopVolumeCurveBehavior` to control how the frame count behaves when looping:\n\n- `\"repeat\"`: Frame count resets to 0 each loop (for `volume` callback)\n- `\"extend\"`: Frame count continues incrementing\n\n```tsx\n<Video\n  src={staticFile(\"video.mp4\")}\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<Video\n  src={staticFile(\"video.mp4\")}\n  toneFrequency={1.5} // Higher pitch\n/>\n<Video\n  src={staticFile(\"video.mp4\")}\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"
  },
  {
    "path": "skills/shadcn/SKILL.md",
    "content": "---\nname: shadcn\ndescription: 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.\n---\n\n# @json-render/shadcn\n\nPre-built shadcn/ui component definitions and implementations for json-render. Provides 36 components built on Radix UI + Tailwind CSS.\n\n## Two Entry Points\n\n| Entry Point | Exports | Use For |\n|-------------|---------|---------|\n| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` | Catalog schemas (no React dependency, safe for server) |\n| `@json-render/shadcn` | `shadcnComponents` | React implementations |\n\n## Usage Pattern\n\nPick the components you need from the standard definitions. Do not spread all definitions -- explicitly select what your app uses:\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { shadcnComponentDefinitions } from \"@json-render/shadcn/catalog\";\nimport { defineRegistry } from \"@json-render/react\";\nimport { shadcnComponents } from \"@json-render/shadcn\";\n\n// Catalog: pick definitions\nconst catalog = defineCatalog(schema, {\n  components: {\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n    Heading: shadcnComponentDefinitions.Heading,\n    Button: shadcnComponentDefinitions.Button,\n    Input: shadcnComponentDefinitions.Input,\n  },\n  actions: {},\n});\n\n// Registry: pick matching implementations\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Heading: shadcnComponents.Heading,\n    Button: shadcnComponents.Button,\n    Input: shadcnComponents.Input,\n  },\n});\n```\n\n> State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. No need to declare them.\n\n## Extending with Custom Components\n\nAdd custom components alongside standard ones:\n\n```typescript\nconst catalog = defineCatalog(schema, {\n  components: {\n    // Standard\n    Card: shadcnComponentDefinitions.Card,\n    Stack: shadcnComponentDefinitions.Stack,\n\n    // Custom\n    Metric: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        trend: z.enum([\"up\", \"down\", \"neutral\"]).nullable(),\n      }),\n      description: \"KPI metric display\",\n    },\n  },\n  actions: {},\n});\n\nconst { registry } = defineRegistry(catalog, {\n  components: {\n    Card: shadcnComponents.Card,\n    Stack: shadcnComponents.Stack,\n    Metric: ({ props }) => <div>{props.label}: {props.value}</div>,\n  },\n});\n```\n\n## Available Components\n\n### Layout\n- **Card** - Container with optional title, description, maxWidth, centered\n- **Stack** - Flex container with direction, gap, align, justify\n- **Grid** - Grid layout with columns (number) and gap\n- **Separator** - Visual divider with orientation\n\n### Navigation\n- **Tabs** - Tabbed navigation with tabs array, defaultValue, value\n- **Accordion** - Collapsible sections with items array and type (single/multiple)\n- **Collapsible** - Single collapsible section with title\n- **Pagination** - Page navigation with totalPages and page\n\n### Overlay\n- **Dialog** - Modal dialog with title, description, openPath\n- **Drawer** - Bottom drawer with title, description, openPath\n- **Tooltip** - Hover tooltip with content and text\n- **Popover** - Click-triggered popover with trigger and content\n- **DropdownMenu** - Dropdown with label and items array\n\n### Content\n- **Heading** - Heading text with level (h1-h4)\n- **Text** - Paragraph with variant (body, caption, muted, lead, code)\n- **Image** - Image with alt, width, height\n- **Avatar** - User avatar with src, name, size\n- **Badge** - Status badge with text and variant (default, secondary, destructive, outline)\n- **Alert** - Alert banner with title, message, type (success, warning, info, error)\n- **Carousel** - Scrollable carousel with items array\n- **Table** - Data table with columns (string[]) and rows (string[][])\n\n### Feedback\n- **Progress** - Progress bar with value, max, label\n- **Skeleton** - Loading placeholder with width, height, rounded\n- **Spinner** - Loading spinner with size and label\n\n### Input\n- **Button** - Button with label, variant (primary, secondary, danger), disabled\n- **Link** - Anchor link with label and href\n- **Input** - Text input with label, name, type, placeholder, value, checks\n- **Textarea** - Multi-line input with label, name, placeholder, rows, value, checks\n- **Select** - Dropdown select with label, name, options (string[]), value, checks\n- **Checkbox** - Checkbox with label, name, checked, checks, validateOn\n- **Radio** - Radio group with label, name, options (string[]), value, checks, validateOn\n- **Switch** - Toggle switch with label, name, checked, checks, validateOn\n- **Slider** - Range slider with label, min, max, step, value\n- **Toggle** - Toggle button with label, pressed, variant\n- **ToggleGroup** - Group of toggles with items, type, value\n- **ButtonGroup** - Button group with buttons array and selected\n\n## Built-in Actions (from `@json-render/react`)\n\nThese are built into the React schema and handled by `ActionProvider` automatically. They appear in prompts without needing to be declared in the catalog.\n\n- **setState** - Set a value at a state path (`{ statePath, value }`)\n- **pushState** - Push a value onto an array (`{ statePath, value, clearStatePath? }`)\n- **removeState** - Remove an array item by index (`{ statePath, index }`)\n- **validateForm** - Validate all fields, write `{ valid, errors }` to state (`{ statePath? }`)\n\n## Validation Timing (`validateOn`)\n\nAll form components support `validateOn` to control when validation runs:\n- `\"change\"` — validate on every input change (default for Select, Checkbox, Radio, Switch)\n- `\"blur\"` — validate when field loses focus (default for Input, Textarea)\n- `\"submit\"` — validate only on form submission\n\n## Important Notes\n\n- The `/catalog` entry point has no React dependency -- use it for server-side prompt generation\n- Components use Tailwind CSS classes -- your app must have Tailwind configured\n- Component implementations use bundled shadcn/ui primitives (not your app's `components/ui/`)\n- All form inputs support `checks` for validation (type + message pairs) and `validateOn` for timing\n- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`\n"
  },
  {
    "path": "skills/skill-creator/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "skills/skill-creator/SKILL.md",
    "content": "---\nname: skill-creator\ndescription: 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.\nlicense: Complete terms in LICENSE.txt\n---\n\n# Skill Creator\n\nThis skill provides guidance for creating effective skills.\n\n## About Skills\n\nSkills are modular, self-contained packages that extend Claude's capabilities by providing\nspecialized knowledge, workflows, and tools. Think of them as \"onboarding guides\" for specific\ndomains or tasks—they transform Claude from a general-purpose agent into a specialized agent\nequipped with procedural knowledge that no model can fully possess.\n\n### What Skills Provide\n\n1. Specialized workflows - Multi-step procedures for specific domains\n2. Tool integrations - Instructions for working with specific file formats or APIs\n3. Domain expertise - Company-specific knowledge, schemas, business logic\n4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks\n\n## Core Principles\n\n### Concise is Key\n\nThe context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.\n\n**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: \"Does Claude really need this explanation?\" and \"Does this paragraph justify its token cost?\"\n\nPrefer concise examples over verbose explanations.\n\n### Set Appropriate Degrees of Freedom\n\nMatch the level of specificity to the task's fragility and variability:\n\n**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.\n\n**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.\n\n**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.\n\nThink of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).\n\n### Anatomy of a Skill\n\nEvery skill consists of a required SKILL.md file and optional bundled resources:\n\n```\nskill-name/\n├── SKILL.md (required)\n│   ├── YAML frontmatter metadata (required)\n│   │   ├── name: (required)\n│   │   └── description: (required)\n│   └── Markdown instructions (required)\n└── Bundled Resources (optional)\n    ├── scripts/          - Executable code (Python/Bash/etc.)\n    ├── references/       - Documentation intended to be loaded into context as needed\n    └── assets/           - Files used in output (templates, icons, fonts, etc.)\n```\n\n#### SKILL.md (required)\n\nEvery SKILL.md consists of:\n\n- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.\n- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).\n\n#### Bundled Resources (optional)\n\n##### Scripts (`scripts/`)\n\nExecutable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.\n\n- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed\n- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks\n- **Benefits**: Token efficient, deterministic, may be executed without loading into context\n- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments\n\n##### References (`references/`)\n\nDocumentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.\n\n- **When to include**: For documentation that Claude should reference while working\n- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications\n- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides\n- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed\n- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md\n- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.\n\n##### Assets (`assets/`)\n\nFiles not intended to be loaded into context, but rather used within the output Claude produces.\n\n- **When to include**: When the skill needs files that will be used in the final output\n- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography\n- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified\n- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context\n\n#### What to Not Include in a Skill\n\nA skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:\n\n- README.md\n- INSTALLATION_GUIDE.md\n- QUICK_REFERENCE.md\n- CHANGELOG.md\n- etc.\n\nThe skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.\n\n### Progressive Disclosure Design Principle\n\nSkills use a three-level loading system to manage context efficiently:\n\n1. **Metadata (name + description)** - Always in context (~100 words)\n2. **SKILL.md body** - When skill triggers (<5k words)\n3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)\n\n#### Progressive Disclosure Patterns\n\nKeep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.\n\n**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.\n\n**Pattern 1: High-level guide with references**\n\n```markdown\n# PDF Processing\n\n## Quick start\n\nExtract text with pdfplumber:\n[code example]\n\n## Advanced features\n\n- **Form filling**: See [FORMS.md](FORMS.md) for complete guide\n- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods\n- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns\n```\n\nClaude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.\n\n**Pattern 2: Domain-specific organization**\n\nFor Skills with multiple domains, organize content by domain to avoid loading irrelevant context:\n\n```\nbigquery-skill/\n├── SKILL.md (overview and navigation)\n└── reference/\n    ├── finance.md (revenue, billing metrics)\n    ├── sales.md (opportunities, pipeline)\n    ├── product.md (API usage, features)\n    └── marketing.md (campaigns, attribution)\n```\n\nWhen a user asks about sales metrics, Claude only reads sales.md.\n\nSimilarly, for skills supporting multiple frameworks or variants, organize by variant:\n\n```\ncloud-deploy/\n├── SKILL.md (workflow + provider selection)\n└── references/\n    ├── aws.md (AWS deployment patterns)\n    ├── gcp.md (GCP deployment patterns)\n    └── azure.md (Azure deployment patterns)\n```\n\nWhen the user chooses AWS, Claude only reads aws.md.\n\n**Pattern 3: Conditional details**\n\nShow basic content, link to advanced content:\n\n```markdown\n# DOCX Processing\n\n## Creating documents\n\nUse docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).\n\n## Editing documents\n\nFor simple edits, modify the XML directly.\n\n**For tracked changes**: See [REDLINING.md](REDLINING.md)\n**For OOXML details**: See [OOXML.md](OOXML.md)\n```\n\nClaude reads REDLINING.md or OOXML.md only when the user needs those features.\n\n**Important guidelines:**\n\n- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.\n- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.\n\n## Skill Creation Process\n\nSkill creation involves these steps:\n\n1. Understand the skill with concrete examples\n2. Plan reusable skill contents (scripts, references, assets)\n3. Initialize the skill (run init_skill.py)\n4. Edit the skill (implement resources and write SKILL.md)\n5. Package the skill (run package_skill.py)\n6. Iterate based on real usage\n\nFollow these steps in order, skipping only if there is a clear reason why they are not applicable.\n\n### Step 1: Understanding the Skill with Concrete Examples\n\nSkip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.\n\nTo create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.\n\nFor example, when building an image-editor skill, relevant questions include:\n\n- \"What functionality should the image-editor skill support? Editing, rotating, anything else?\"\n- \"Can you give some examples of how this skill would be used?\"\n- \"I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?\"\n- \"What would a user say that should trigger this skill?\"\n\nTo avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.\n\nConclude this step when there is a clear sense of the functionality the skill should support.\n\n### Step 2: Planning the Reusable Skill Contents\n\nTo turn concrete examples into an effective skill, analyze each example by:\n\n1. Considering how to execute on the example from scratch\n2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly\n\nExample: When building a `pdf-editor` skill to handle queries like \"Help me rotate this PDF,\" the analysis shows:\n\n1. Rotating a PDF requires re-writing the same code each time\n2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill\n\nExample: When designing a `frontend-webapp-builder` skill for queries like \"Build me a todo app\" or \"Build me a dashboard to track my steps,\" the analysis shows:\n\n1. Writing a frontend webapp requires the same boilerplate HTML/React each time\n2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill\n\nExample: When building a `big-query` skill to handle queries like \"How many users have logged in today?\" the analysis shows:\n\n1. Querying BigQuery requires re-discovering the table schemas and relationships each time\n2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill\n\nTo establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.\n\n### Step 3: Initializing the Skill\n\nAt this point, it is time to actually create the skill.\n\nSkip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.\n\nWhen creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.\n\nUsage:\n\n```bash\nscripts/init_skill.py <skill-name> --path <output-directory>\n```\n\nThe script:\n\n- Creates the skill directory at the specified path\n- Generates a SKILL.md template with proper frontmatter and TODO placeholders\n- Creates example resource directories: `scripts/`, `references/`, and `assets/`\n- Adds example files in each directory that can be customized or deleted\n\nAfter initialization, customize or remove the generated SKILL.md and example files as needed.\n\n### Step 4: Edit the Skill\n\nWhen editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.\n\n#### Learn Proven Design Patterns\n\nConsult these helpful guides based on your skill's needs:\n\n- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic\n- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns\n\nThese files contain established best practices for effective skill design.\n\n#### Start with Reusable Skill Contents\n\nTo begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.\n\nAdded scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.\n\nAny example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.\n\n#### Update SKILL.md\n\n**Writing Guidelines:** Always use imperative/infinitive form.\n\n##### Frontmatter\n\nWrite the YAML frontmatter with `name` and `description`:\n\n- `name`: The skill name\n- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.\n  - Include both what the Skill does and specific triggers/contexts for when to use it.\n  - Include all \"when to use\" information here - Not in the body. The body is only loaded after triggering, so \"When to Use This Skill\" sections in the body are not helpful to Claude.\n  - Example description for a `docx` skill: \"Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks\"\n\nDo not include any other fields in YAML frontmatter.\n\n##### Body\n\nWrite instructions for using the skill and its bundled resources.\n\n### Step 5: Packaging a Skill\n\nOnce development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:\n\n```bash\nscripts/package_skill.py <path/to/skill-folder>\n```\n\nOptional output directory specification:\n\n```bash\nscripts/package_skill.py <path/to/skill-folder> ./dist\n```\n\nThe packaging script will:\n\n1. **Validate** the skill automatically, checking:\n\n   - YAML frontmatter format and required fields\n   - Skill naming conventions and directory structure\n   - Description completeness and quality\n   - File organization and resource references\n\n2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.\n\nIf validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.\n\n### Step 6: Iterate\n\nAfter testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.\n\n**Iteration workflow:**\n\n1. Use the skill on real tasks\n2. Notice struggles or inefficiencies\n3. Identify how SKILL.md or bundled resources should be updated\n4. Implement changes and test again\n"
  },
  {
    "path": "skills/skill-creator/references/output-patterns.md",
    "content": "# Output Patterns\n\nUse these patterns when skills need to produce consistent, high-quality output.\n\n## Template Pattern\n\nProvide templates for output format. Match the level of strictness to your needs.\n\n**For strict requirements (like API responses or data formats):**\n\n```markdown\n## Report structure\n\nALWAYS use this exact template structure:\n\n# [Analysis Title]\n\n## Executive summary\n[One-paragraph overview of key findings]\n\n## Key findings\n- Finding 1 with supporting data\n- Finding 2 with supporting data\n- Finding 3 with supporting data\n\n## Recommendations\n1. Specific actionable recommendation\n2. Specific actionable recommendation\n```\n\n**For flexible guidance (when adaptation is useful):**\n\n```markdown\n## Report structure\n\nHere is a sensible default format, but use your best judgment:\n\n# [Analysis Title]\n\n## Executive summary\n[Overview]\n\n## Key findings\n[Adapt sections based on what you discover]\n\n## Recommendations\n[Tailor to the specific context]\n\nAdjust sections as needed for the specific analysis type.\n```\n\n## Examples Pattern\n\nFor skills where output quality depends on seeing examples, provide input/output pairs:\n\n```markdown\n## Commit message format\n\nGenerate commit messages following these examples:\n\n**Example 1:**\nInput: Added user authentication with JWT tokens\nOutput:\n```\nfeat(auth): implement JWT-based authentication\n\nAdd login endpoint and token validation middleware\n```\n\n**Example 2:**\nInput: Fixed bug where dates displayed incorrectly in reports\nOutput:\n```\nfix(reports): correct date formatting in timezone conversion\n\nUse UTC timestamps consistently across report generation\n```\n\nFollow this style: type(scope): brief description, then detailed explanation.\n```\n\nExamples help Claude understand the desired style and level of detail more clearly than descriptions alone.\n"
  },
  {
    "path": "skills/skill-creator/references/workflows.md",
    "content": "# Workflow Patterns\n\n## Sequential Workflows\n\nFor complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:\n\n```markdown\nFilling a PDF form involves these steps:\n\n1. Analyze the form (run analyze_form.py)\n2. Create field mapping (edit fields.json)\n3. Validate mapping (run validate_fields.py)\n4. Fill the form (run fill_form.py)\n5. Verify output (run verify_output.py)\n```\n\n## Conditional Workflows\n\nFor tasks with branching logic, guide Claude through decision points:\n\n```markdown\n1. Determine the modification type:\n   **Creating new content?** → Follow \"Creation workflow\" below\n   **Editing existing content?** → Follow \"Editing workflow\" below\n\n2. Creation workflow: [steps]\n3. Editing workflow: [steps]\n```"
  },
  {
    "path": "skills/skill-creator/scripts/init_skill.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nSkill Initializer - Creates a new skill from template\n\nUsage:\n    init_skill.py <skill-name> --path <path>\n\nExamples:\n    init_skill.py my-new-skill --path skills/public\n    init_skill.py my-api-helper --path skills/private\n    init_skill.py custom-skill --path /custom/location\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\n\nSKILL_TEMPLATE = \"\"\"---\nname: {skill_name}\ndescription: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]\n---\n\n# {skill_title}\n\n## Overview\n\n[TODO: 1-2 sentences explaining what this skill enables]\n\n## Structuring This Skill\n\n[TODO: Choose the structure that best fits this skill's purpose. Common patterns:\n\n**1. Workflow-Based** (best for sequential processes)\n- Works well when there are clear step-by-step procedures\n- Example: DOCX skill with \"Workflow Decision Tree\" → \"Reading\" → \"Creating\" → \"Editing\"\n- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...\n\n**2. Task-Based** (best for tool collections)\n- Works well when the skill offers different operations/capabilities\n- Example: PDF skill with \"Quick Start\" → \"Merge PDFs\" → \"Split PDFs\" → \"Extract Text\"\n- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...\n\n**3. Reference/Guidelines** (best for standards or specifications)\n- Works well for brand guidelines, coding standards, or requirements\n- Example: Brand styling with \"Brand Guidelines\" → \"Colors\" → \"Typography\" → \"Features\"\n- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...\n\n**4. Capabilities-Based** (best for integrated systems)\n- Works well when the skill provides multiple interrelated features\n- Example: Product Management with \"Core Capabilities\" → numbered capability list\n- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...\n\nPatterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).\n\nDelete this entire \"Structuring This Skill\" section when done - it's just guidance.]\n\n## [TODO: Replace with the first main section based on chosen structure]\n\n[TODO: Add content here. See examples in existing skills:\n- Code samples for technical skills\n- Decision trees for complex workflows\n- Concrete examples with realistic user requests\n- References to scripts/templates/references as needed]\n\n## Resources\n\nThis skill includes example resource directories that demonstrate how to organize different types of bundled resources:\n\n### scripts/\nExecutable code (Python/Bash/etc.) that can be run directly to perform specific operations.\n\n**Examples from other skills:**\n- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation\n- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing\n\n**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.\n\n**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments.\n\n### references/\nDocumentation and reference material intended to be loaded into context to inform Claude's process and thinking.\n\n**Examples from other skills:**\n- Product management: `communication.md`, `context_building.md` - detailed workflow guides\n- BigQuery: API reference documentation and query examples\n- Finance: Schema documentation, company policies\n\n**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working.\n\n### assets/\nFiles not intended to be loaded into context, but rather used within the output Claude produces.\n\n**Examples from other skills:**\n- Brand styling: PowerPoint template files (.pptx), logo files\n- Frontend builder: HTML/React boilerplate project directories\n- Typography: Font files (.ttf, .woff2)\n\n**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.\n\n---\n\n**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.\n\"\"\"\n\nEXAMPLE_SCRIPT = '''#!/usr/bin/env python3\n\"\"\"\nExample helper script for {skill_name}\n\nThis is a placeholder script that can be executed directly.\nReplace with actual implementation or delete if not needed.\n\nExample real scripts from other skills:\n- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields\n- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images\n\"\"\"\n\ndef main():\n    print(\"This is an example script for {skill_name}\")\n    # TODO: Add actual script logic here\n    # This could be data processing, file conversion, API calls, etc.\n\nif __name__ == \"__main__\":\n    main()\n'''\n\nEXAMPLE_REFERENCE = \"\"\"# Reference Documentation for {skill_title}\n\nThis is a placeholder for detailed reference documentation.\nReplace with actual reference content or delete if not needed.\n\nExample real reference docs from other skills:\n- product-management/references/communication.md - Comprehensive guide for status updates\n- product-management/references/context_building.md - Deep-dive on gathering context\n- bigquery/references/ - API references and query examples\n\n## When Reference Docs Are Useful\n\nReference docs are ideal for:\n- Comprehensive API documentation\n- Detailed workflow guides\n- Complex multi-step processes\n- Information too lengthy for main SKILL.md\n- Content that's only needed for specific use cases\n\n## Structure Suggestions\n\n### API Reference Example\n- Overview\n- Authentication\n- Endpoints with examples\n- Error codes\n- Rate limits\n\n### Workflow Guide Example\n- Prerequisites\n- Step-by-step instructions\n- Common patterns\n- Troubleshooting\n- Best practices\n\"\"\"\n\nEXAMPLE_ASSET = \"\"\"# Example Asset File\n\nThis placeholder represents where asset files would be stored.\nReplace with actual asset files (templates, images, fonts, etc.) or delete if not needed.\n\nAsset files are NOT intended to be loaded into context, but rather used within\nthe output Claude produces.\n\nExample asset files from other skills:\n- Brand guidelines: logo.png, slides_template.pptx\n- Frontend builder: hello-world/ directory with HTML/React boilerplate\n- Typography: custom-font.ttf, font-family.woff2\n- Data: sample_data.csv, test_dataset.json\n\n## Common Asset Types\n\n- Templates: .pptx, .docx, boilerplate directories\n- Images: .png, .jpg, .svg, .gif\n- Fonts: .ttf, .otf, .woff, .woff2\n- Boilerplate code: Project directories, starter files\n- Icons: .ico, .svg\n- Data files: .csv, .json, .xml, .yaml\n\nNote: This is a text placeholder. Actual assets can be any file type.\n\"\"\"\n\n\ndef title_case_skill_name(skill_name):\n    \"\"\"Convert hyphenated skill name to Title Case for display.\"\"\"\n    return ' '.join(word.capitalize() for word in skill_name.split('-'))\n\n\ndef init_skill(skill_name, path):\n    \"\"\"\n    Initialize a new skill directory with template SKILL.md.\n\n    Args:\n        skill_name: Name of the skill\n        path: Path where the skill directory should be created\n\n    Returns:\n        Path to created skill directory, or None if error\n    \"\"\"\n    # Determine skill directory path\n    skill_dir = Path(path).resolve() / skill_name\n\n    # Check if directory already exists\n    if skill_dir.exists():\n        print(f\"❌ Error: Skill directory already exists: {skill_dir}\")\n        return None\n\n    # Create skill directory\n    try:\n        skill_dir.mkdir(parents=True, exist_ok=False)\n        print(f\"✅ Created skill directory: {skill_dir}\")\n    except Exception as e:\n        print(f\"❌ Error creating directory: {e}\")\n        return None\n\n    # Create SKILL.md from template\n    skill_title = title_case_skill_name(skill_name)\n    skill_content = SKILL_TEMPLATE.format(\n        skill_name=skill_name,\n        skill_title=skill_title\n    )\n\n    skill_md_path = skill_dir / 'SKILL.md'\n    try:\n        skill_md_path.write_text(skill_content)\n        print(\"✅ Created SKILL.md\")\n    except Exception as e:\n        print(f\"❌ Error creating SKILL.md: {e}\")\n        return None\n\n    # Create resource directories with example files\n    try:\n        # Create scripts/ directory with example script\n        scripts_dir = skill_dir / 'scripts'\n        scripts_dir.mkdir(exist_ok=True)\n        example_script = scripts_dir / 'example.py'\n        example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))\n        example_script.chmod(0o755)\n        print(\"✅ Created scripts/example.py\")\n\n        # Create references/ directory with example reference doc\n        references_dir = skill_dir / 'references'\n        references_dir.mkdir(exist_ok=True)\n        example_reference = references_dir / 'api_reference.md'\n        example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))\n        print(\"✅ Created references/api_reference.md\")\n\n        # Create assets/ directory with example asset placeholder\n        assets_dir = skill_dir / 'assets'\n        assets_dir.mkdir(exist_ok=True)\n        example_asset = assets_dir / 'example_asset.txt'\n        example_asset.write_text(EXAMPLE_ASSET)\n        print(\"✅ Created assets/example_asset.txt\")\n    except Exception as e:\n        print(f\"❌ Error creating resource directories: {e}\")\n        return None\n\n    # Print next steps\n    print(f\"\\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}\")\n    print(\"\\nNext steps:\")\n    print(\"1. Edit SKILL.md to complete the TODO items and update the description\")\n    print(\"2. Customize or delete the example files in scripts/, references/, and assets/\")\n    print(\"3. Run the validator when ready to check the skill structure\")\n\n    return skill_dir\n\n\ndef main():\n    if len(sys.argv) < 4 or sys.argv[2] != '--path':\n        print(\"Usage: init_skill.py <skill-name> --path <path>\")\n        print(\"\\nSkill name requirements:\")\n        print(\"  - Hyphen-case identifier (e.g., 'data-analyzer')\")\n        print(\"  - Lowercase letters, digits, and hyphens only\")\n        print(\"  - Max 40 characters\")\n        print(\"  - Must match directory name exactly\")\n        print(\"\\nExamples:\")\n        print(\"  init_skill.py my-new-skill --path skills/public\")\n        print(\"  init_skill.py my-api-helper --path skills/private\")\n        print(\"  init_skill.py custom-skill --path /custom/location\")\n        sys.exit(1)\n\n    skill_name = sys.argv[1]\n    path = sys.argv[3]\n\n    print(f\"🚀 Initializing skill: {skill_name}\")\n    print(f\"   Location: {path}\")\n    print()\n\n    result = init_skill(skill_name, path)\n\n    if result:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/skill-creator/scripts/package_skill.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nSkill Packager - Creates a distributable .skill file of a skill folder\n\nUsage:\n    python utils/package_skill.py <path/to/skill-folder> [output-directory]\n\nExample:\n    python utils/package_skill.py skills/public/my-skill\n    python utils/package_skill.py skills/public/my-skill ./dist\n\"\"\"\n\nimport sys\nimport zipfile\nfrom pathlib import Path\nfrom quick_validate import validate_skill\n\n\ndef package_skill(skill_path, output_dir=None):\n    \"\"\"\n    Package a skill folder into a .skill file.\n\n    Args:\n        skill_path: Path to the skill folder\n        output_dir: Optional output directory for the .skill file (defaults to current directory)\n\n    Returns:\n        Path to the created .skill file, or None if error\n    \"\"\"\n    skill_path = Path(skill_path).resolve()\n\n    # Validate skill folder exists\n    if not skill_path.exists():\n        print(f\"❌ Error: Skill folder not found: {skill_path}\")\n        return None\n\n    if not skill_path.is_dir():\n        print(f\"❌ Error: Path is not a directory: {skill_path}\")\n        return None\n\n    # Validate SKILL.md exists\n    skill_md = skill_path / \"SKILL.md\"\n    if not skill_md.exists():\n        print(f\"❌ Error: SKILL.md not found in {skill_path}\")\n        return None\n\n    # Run validation before packaging\n    print(\"🔍 Validating skill...\")\n    valid, message = validate_skill(skill_path)\n    if not valid:\n        print(f\"❌ Validation failed: {message}\")\n        print(\"   Please fix the validation errors before packaging.\")\n        return None\n    print(f\"✅ {message}\\n\")\n\n    # Determine output location\n    skill_name = skill_path.name\n    if output_dir:\n        output_path = Path(output_dir).resolve()\n        output_path.mkdir(parents=True, exist_ok=True)\n    else:\n        output_path = Path.cwd()\n\n    skill_filename = output_path / f\"{skill_name}.skill\"\n\n    # Create the .skill file (zip format)\n    try:\n        with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:\n            # Walk through the skill directory\n            for file_path in skill_path.rglob('*'):\n                if file_path.is_file():\n                    # Calculate the relative path within the zip\n                    arcname = file_path.relative_to(skill_path.parent)\n                    zipf.write(file_path, arcname)\n                    print(f\"  Added: {arcname}\")\n\n        print(f\"\\n✅ Successfully packaged skill to: {skill_filename}\")\n        return skill_filename\n\n    except Exception as e:\n        print(f\"❌ Error creating .skill file: {e}\")\n        return None\n\n\ndef main():\n    if len(sys.argv) < 2:\n        print(\"Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]\")\n        print(\"\\nExample:\")\n        print(\"  python utils/package_skill.py skills/public/my-skill\")\n        print(\"  python utils/package_skill.py skills/public/my-skill ./dist\")\n        sys.exit(1)\n\n    skill_path = sys.argv[1]\n    output_dir = sys.argv[2] if len(sys.argv) > 2 else None\n\n    print(f\"📦 Packaging skill: {skill_path}\")\n    if output_dir:\n        print(f\"   Output directory: {output_dir}\")\n    print()\n\n    result = package_skill(skill_path, output_dir)\n\n    if result:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "skills/skill-creator/scripts/quick_validate.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nQuick validation script for skills - minimal version\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport yaml\nfrom pathlib import Path\n\ndef validate_skill(skill_path):\n    \"\"\"Basic validation of a skill\"\"\"\n    skill_path = Path(skill_path)\n\n    # Check SKILL.md exists\n    skill_md = skill_path / 'SKILL.md'\n    if not skill_md.exists():\n        return False, \"SKILL.md not found\"\n\n    # Read and validate frontmatter\n    content = skill_md.read_text()\n    if not content.startswith('---'):\n        return False, \"No YAML frontmatter found\"\n\n    # Extract frontmatter\n    match = re.match(r'^---\\n(.*?)\\n---', content, re.DOTALL)\n    if not match:\n        return False, \"Invalid frontmatter format\"\n\n    frontmatter_text = match.group(1)\n\n    # Parse YAML frontmatter\n    try:\n        frontmatter = yaml.safe_load(frontmatter_text)\n        if not isinstance(frontmatter, dict):\n            return False, \"Frontmatter must be a YAML dictionary\"\n    except yaml.YAMLError as e:\n        return False, f\"Invalid YAML in frontmatter: {e}\"\n\n    # Define allowed properties\n    ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}\n\n    # Check for unexpected properties (excluding nested keys under metadata)\n    unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES\n    if unexpected_keys:\n        return False, (\n            f\"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. \"\n            f\"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}\"\n        )\n\n    # Check required fields\n    if 'name' not in frontmatter:\n        return False, \"Missing 'name' in frontmatter\"\n    if 'description' not in frontmatter:\n        return False, \"Missing 'description' in frontmatter\"\n\n    # Extract name for validation\n    name = frontmatter.get('name', '')\n    if not isinstance(name, str):\n        return False, f\"Name must be a string, got {type(name).__name__}\"\n    name = name.strip()\n    if name:\n        # Check naming convention (hyphen-case: lowercase with hyphens)\n        if not re.match(r'^[a-z0-9-]+$', name):\n            return False, f\"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)\"\n        if name.startswith('-') or name.endswith('-') or '--' in name:\n            return False, f\"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens\"\n        # Check name length (max 64 characters per spec)\n        if len(name) > 64:\n            return False, f\"Name is too long ({len(name)} characters). Maximum is 64 characters.\"\n\n    # Extract and validate description\n    description = frontmatter.get('description', '')\n    if not isinstance(description, str):\n        return False, f\"Description must be a string, got {type(description).__name__}\"\n    description = description.strip()\n    if description:\n        # Check for angle brackets\n        if '<' in description or '>' in description:\n            return False, \"Description cannot contain angle brackets (< or >)\"\n        # Check description length (max 1024 characters per spec)\n        if len(description) > 1024:\n            return False, f\"Description is too long ({len(description)} characters). Maximum is 1024 characters.\"\n\n    return True, \"Skill is valid!\"\n\nif __name__ == \"__main__\":\n    if len(sys.argv) != 2:\n        print(\"Usage: python quick_validate.py <skill_directory>\")\n        sys.exit(1)\n    \n    valid, message = validate_skill(sys.argv[1])\n    print(message)\n    sys.exit(0 if valid else 1)"
  },
  {
    "path": "skills/solid/SKILL.md",
    "content": "---\nname: solid\ndescription: 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.\n---\n\n# @json-render/solid\n\n`@json-render/solid` renders json-render specs into Solid component trees with fine-grained reactivity.\n\n## Quick Start\n\n```tsx\nimport { Renderer, JSONUIProvider } from \"@json-render/solid\";\nimport type { Spec } from \"@json-render/solid\";\nimport { registry } from \"./registry\";\n\nexport function App(props: { spec: Spec | null }) {\n  return (\n    <JSONUIProvider registry={registry} initialState={{}}>\n      <Renderer spec={props.spec} registry={registry} />\n    </JSONUIProvider>\n  );\n}\n```\n\n## Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/solid/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\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      description: \"Card container\",\n    },\n  },\n  actions: {\n    submit: { description: \"Submit data\" },\n  },\n});\n```\n\n## Define Components\n\nComponents receive `ComponentRenderProps` from the renderer:\n\n```ts\ninterface ComponentRenderProps<P = Record<string, unknown>> {\n  element: UIElement<string, P>;\n  children?: JSX.Element;\n  emit: (event: string) => void;\n  on: (event: string) => EventHandle;\n  bindings?: Record<string, string>;\n  loading?: boolean;\n}\n```\n\nExample:\n\n```tsx\nimport type { BaseComponentProps } from \"@json-render/solid\";\n\nexport function Button(props: BaseComponentProps<{ label: string }>) {\n  return (\n    <button onClick={() => props.emit(\"press\")}>{props.props.label}</button>\n  );\n}\n```\n\n## Create a Registry\n\n```typescript\nimport { defineRegistry } from \"@json-render/solid\";\nimport { catalog } from \"./catalog\";\nimport { Card } from \"./Card\";\nimport { Button } from \"./Button\";\n\nconst { registry, handlers, executeAction } = defineRegistry(catalog, {\n  components: {\n    Card,\n    Button,\n  },\n  actions: {\n    submit: async (params, setState, state) => {\n      // custom action logic\n    },\n  },\n});\n```\n\n## Spec Structure\n\n```json\n{\n  \"root\": \"card1\",\n  \"elements\": {\n    \"card1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Hello\" },\n      \"children\": [\"btn1\"]\n    },\n    \"btn1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Click me\" },\n      \"on\": {\n        \"press\": { \"action\": \"submit\" }\n      }\n    }\n  }\n}\n```\n\n## Providers\n\n- `StateProvider`: state model read/write and controlled mode via `store`\n- `VisibilityProvider`: evaluates `visible` conditions\n- `ValidationProvider`: field validation + `validateForm` integration\n- `ActionProvider`: runs built-in and custom actions\n- `JSONUIProvider`: combined provider wrapper\n\n## Hooks\n\n- `useStateStore`, `useStateValue`, `useStateBinding`\n- `useVisibility`, `useIsVisible`\n- `useActions`, `useAction`\n- `useValidation`, `useOptionalValidation`, `useFieldValidation`\n- `useBoundProp`\n- `useUIStream`, `useChatUI`\n\n## Built-in Actions\n\nHandled automatically by `ActionProvider`:\n\n- `setState`\n- `pushState`\n- `removeState`\n- `validateForm`\n\n## Dynamic Props and Bindings\n\nSupported expression forms include:\n\n- `{\"$state\": \"/path\"}`\n- `{\"$bindState\": \"/path\"}`\n- `{\"$bindItem\": \"field\"}`\n- `{\"$template\": \"Hi ${/user/name}\"}`\n- `{\"$computed\": \"fn\", \"args\": {...}}`\n- `{\"$cond\": <condition>, \"$then\": <value>, \"$else\": <value>}`\n\nUse `useBoundProp` in components for writable bound values:\n\n```tsx\nimport { useBoundProp } from \"@json-render/solid\";\n\nfunction Input(props: BaseComponentProps<{ value?: string }>) {\n  const [value, setValue] = useBoundProp(\n    props.props.value,\n    props.bindings?.value,\n  );\n  return (\n    <input\n      value={String(value() ?? \"\")}\n      onInput={(e) => setValue(e.currentTarget.value)}\n    />\n  );\n}\n```\n\n`useStateValue`, `useStateBinding`, and the `state` / `errors` / `isValid` fields from `useFieldValidation` are reactive accessors in Solid. Call them as functions inside JSX, `createMemo`, or `createEffect`.\n\n## Solid Reactivity Rules\n\n- Do not destructure component props in function signatures when values need to stay reactive.\n- Keep changing reads inside JSX expressions, `createMemo`, or `createEffect`.\n- Context values are exposed through getter-based objects so consumers always observe live signals.\n\n## Streaming UI\n\n```tsx\nimport { useUIStream, Renderer } from \"@json-render/solid\";\n\nconst stream = useUIStream({ api: \"/api/generate-ui\" });\nawait stream.send(\"Create a support dashboard\");\n\n<Renderer\n  spec={stream.spec}\n  registry={registry}\n  loading={stream.isStreaming}\n/>;\n```\n\nUse `useChatUI` for chat + UI generation flows.\n"
  },
  {
    "path": "skills/svelte/SKILL.md",
    "content": "---\nname: svelte\ndescription: 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.\n---\n\n# @json-render/svelte\n\nSvelte 5 renderer that converts json-render specs into Svelte component trees.\n\n## Quick Start\n\n```svelte\n<script lang=\"ts\">\n  import { Renderer, JsonUIProvider } from \"@json-render/svelte\";\n  import type { Spec } from \"@json-render/svelte\";\n  import Card from \"./components/Card.svelte\";\n  import Button from \"./components/Button.svelte\";\n\n  interface Props {\n    spec: Spec | null;\n  }\n\n  let { spec }: Props = $props();\n  const registry = { Card, Button };\n</script>\n\n<JsonUIProvider>\n  <Renderer {spec} {registry} />\n</JsonUIProvider>\n```\n\n## Creating a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/svelte\";\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\",\n    },\n    Card: {\n      props: z.object({ title: z.string() }),\n      description: \"Card container with title\",\n    },\n  },\n});\n```\n\n## Defining Components\n\nComponents should accept `BaseComponentProps<TProps>`:\n\n```typescript\ninterface BaseComponentProps<TProps> {\n  props: TProps; // Resolved props for this component\n  children?: Snippet; // Child elements (use {@render children()})\n  emit: (event: string) => void; // Fire a named event\n  bindings?: Record<string, string>; // Map of prop names to state paths (for $bindState)\n  loading?: boolean; // True while spec is streaming\n}\n```\n\n```svelte\n<!-- Button.svelte -->\n<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ label: string; variant?: string }> {}\n  let { props, emit }: Props = $props();\n</script>\n\n<button class={props.variant} onclick={() => emit(\"press\")}>\n  {props.label}\n</button>\n```\n\n```svelte\n<!-- Card.svelte -->\n<script lang=\"ts\">\n  import type { Snippet } from \"svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ title: string }> {\n    children?: Snippet;\n  }\n\n  let { props, children }: Props = $props();\n</script>\n\n<div class=\"card\">\n  <h2>{props.title}</h2>\n  {#if children}\n    {@render children()}\n  {/if}\n</div>\n```\n\n## Creating a Registry\n\n```typescript\nimport { defineRegistry } from \"@json-render/svelte\";\nimport { catalog } from \"./catalog\";\nimport Card from \"./components/Card.svelte\";\nimport Button from \"./components/Button.svelte\";\n\nconst { registry, handlers, executeAction } = defineRegistry(catalog, {\n  components: {\n    Card,\n    Button,\n  },\n  actions: {\n    submit: async (params, setState, state) => {\n      // handle action\n    },\n  },\n});\n```\n\n## Spec Structure (Element Tree)\n\nThe Svelte schema uses the element tree format:\n\n```json\n{\n  \"root\": \"card1\",\n  \"elements\": {\n    \"card1\": {\n      \"type\": \"Card\",\n      \"props\": { \"title\": \"Hello\" },\n      \"children\": [\"btn1\"]\n    },\n    \"btn1\": {\n      \"type\": \"Button\",\n      \"props\": { \"label\": \"Click me\" }\n    }\n  }\n}\n```\n\n## Visibility Conditions\n\nUse `visible` on elements to show/hide based on state:\n\n- `{ \"$state\": \"/path\" }` - truthy check\n- `{ \"$state\": \"/path\", \"eq\": value }` - equality check\n- `{ \"$state\": \"/path\", \"not\": true }` - falsy check\n- `{ \"$and\": [cond1, cond2] }` - AND conditions\n- `{ \"$or\": [cond1, cond2] }` - OR conditions\n\n## Providers (via JsonUIProvider)\n\n`JsonUIProvider` composes all contexts. Individual contexts:\n\n| Context             | Purpose                                            |\n| ------------------- | -------------------------------------------------- |\n| `StateContext`      | Share state across components (JSON Pointer paths) |\n| `ActionContext`     | Handle actions dispatched via the event system     |\n| `VisibilityContext` | Enable conditional rendering based on state        |\n| `ValidationContext` | Form field validation                              |\n\n## Event System\n\nComponents use `emit` to fire named events. The element's `on` field maps events to action bindings:\n\n```svelte\n<!-- Button.svelte -->\n<script lang=\"ts\">\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ label: string }> {}\n\n  let { props, emit }: Props = $props();\n</script>\n\n<button onclick={() => emit(\"press\")}>{props.label}</button>\n```\n\n```json\n{\n  \"type\": \"Button\",\n  \"props\": { \"label\": \"Submit\" },\n  \"on\": { \"press\": { \"action\": \"submit\" } }\n}\n```\n\n## Built-in Actions\n\nThe `setState` action is handled automatically and updates the state model:\n\n```json\n{\n  \"action\": \"setState\",\n  \"actionParams\": { \"statePath\": \"/activeTab\", \"value\": \"home\" }\n}\n```\n\nOther built-in actions: `pushState`, `removeState`, `push`, `pop`.\n\n## Dynamic Props and Two-Way Binding\n\nExpression forms resolved before your component receives props:\n\n- `{\"$state\": \"/state/key\"}` - read from state\n- `{\"$bindState\": \"/form/email\"}` - read + write-back to state\n- `{\"$bindItem\": \"field\"}` - read + write-back for repeat items\n- `{\"$cond\": <condition>, \"$then\": <value>, \"$else\": <value>}` - conditional value\n\nFor writable bindings inside components, use `getBoundProp`:\n\n```svelte\n<script lang=\"ts\">\n  import { getBoundProp } from \"@json-render/svelte\";\n  import type { BaseComponentProps } from \"@json-render/svelte\";\n\n  interface Props extends BaseComponentProps<{ value?: string }> {}\n  let { props, bindings }: Props = $props();\n\n  let value = getBoundProp<string>(\n    () => props.value,\n    () => bindings?.value,\n  );\n</script>\n\n<input bind:value={value.current} />\n```\n\n## Context Helpers\n\nPreferred helpers:\n\n- `getStateValue(path)` - returns `{ current }` (read/write)\n- `getBoundProp(() => value, () => bindingPath)` - returns `{ current }` (read/write when bound)\n- `isVisible(condition)` - returns `{ current }` (boolean)\n- `getAction(name)` - returns `{ current }` (registered handler)\n\nAdvanced context access:\n\n- `getStateContext()`\n- `getActionContext()`\n- `getVisibilityContext()`\n- `getValidationContext()`\n- `getOptionalValidationContext()`\n- `getFieldValidation(ctx, path, config?)`\n\n## Streaming UI\n\nUse `createUIStream` for spec streaming:\n\n```svelte\n<script lang=\"ts\">\n  import { createUIStream, Renderer } from \"@json-render/svelte\";\n\n  const stream = createUIStream({\n    api: \"/api/generate-ui\",\n    onComplete: (spec) => console.log(\"Done\", spec),\n  });\n\n  async function generate() {\n    await stream.send(\"Create a login form\");\n  }\n</script>\n\n<button onclick={generate} disabled={stream.isStreaming}>\n  {stream.isStreaming ? \"Generating...\" : \"Generate UI\"}\n</button>\n\n{#if stream.spec}\n  <Renderer spec={stream.spec} {registry} loading={stream.isStreaming} />\n{/if}\n```\n\nUse `createChatUI` for chat + UI responses:\n\n```typescript\nconst chat = createChatUI({ api: \"/api/chat-ui\" });\nawait chat.send(\"Build a settings panel\");\n```\n"
  },
  {
    "path": "skills/vue/SKILL.md",
    "content": "---\nname: vue\ndescription: 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.\n---\n\n# @json-render/vue\n\nVue 3 renderer that converts JSON specs into Vue component trees with data binding, visibility, and actions.\n\n## Installation\n\n```bash\nnpm install @json-render/vue @json-render/core zod\n```\n\nPeer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.\n\n## Quick Start\n\n### Create a Catalog\n\n```typescript\nimport { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/vue/schema\";\nimport { z } from \"zod\";\n\nexport const catalog = defineCatalog(schema, {\n  components: {\n    Card: {\n      props: z.object({ title: z.string(), description: z.string().nullable() }),\n      description: \"A card container\",\n    },\n    Button: {\n      props: z.object({ label: z.string(), action: z.string() }),\n      description: \"A clickable button\",\n    },\n  },\n  actions: {},\n});\n```\n\n### Define Registry with h() Render Functions\n\n```typescript\nimport { h } from \"vue\";\nimport { defineRegistry } from \"@json-render/vue\";\nimport { catalog } from \"./catalog\";\n\nexport const { registry } = defineRegistry(catalog, {\n  components: {\n    Card: ({ props, children }) =>\n      h(\"div\", { class: \"card\" }, [\n        h(\"h3\", null, props.title),\n        props.description ? h(\"p\", null, props.description) : null,\n        children,\n      ]),\n    Button: ({ props, emit }) =>\n      h(\"button\", { onClick: () => emit(\"press\") }, props.label),\n  },\n});\n```\n\n### Render Specs\n\n```vue\n<script setup lang=\"ts\">\nimport { StateProvider, ActionProvider, Renderer } from \"@json-render/vue\";\nimport { registry } from \"./registry\";\n\nconst spec = { root: \"card-1\", elements: { /* ... */ } };\n</script>\n\n<template>\n  <StateProvider :initial-state=\"{ form: { name: '' } }\">\n    <ActionProvider :handlers=\"{ submit: handleSubmit }\">\n      <Renderer :spec=\"spec\" :registry=\"registry\" />\n    </ActionProvider>\n  </StateProvider>\n</template>\n```\n\n## Providers\n\n| Provider | Purpose |\n|----------|---------|\n| `StateProvider` | Share state across components (JSON Pointer paths). Accepts `initialState` or `store` 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## Composables\n\n| Composable | Purpose |\n|------------|---------|\n| `useStateStore()` | Access state context (`state` as `ShallowRef`, `get`, `set`, `update`) |\n| `useStateValue(path)` | Get single value from state |\n| `useIsVisible(condition)` | Check if a visibility condition is met |\n| `useActions()` | Access action context |\n| `useAction(binding)` | Get a single action dispatch function |\n| `useFieldValidation(path, config)` | Field validation state |\n| `useBoundProp(propValue, bindingPath)` | Two-way binding for `$bindState`/`$bindItem` |\n\nNote: `useStateStore().state` returns a `ShallowRef<StateModel>` — use `state.value` to access.\n\n## External Store (StateStore)\n\nPass a `StateStore` to `StateProvider` to wire json-render to Pinia, VueUse, or any state management:\n\n```typescript\nimport { createStateStore, type StateStore } from \"@json-render/vue\";\n\nconst store = createStateStore({ count: 0 });\n```\n\n```vue\n<StateProvider :store=\"store\">\n  <Renderer :spec=\"spec\" :registry=\"registry\" />\n</StateProvider>\n```\n\n## Dynamic Prop Expressions\n\nProps support `$state`, `$bindState`, `$cond`, `$template`, `$computed`. Use `{ \"$bindState\": \"/path\" }` on the natural value prop for two-way binding.\n\n## Visibility Conditions\n\n```typescript\n{ \"$state\": \"/user/isAdmin\" }\n{ \"$state\": \"/status\", \"eq\": \"active\" }\n{ \"$state\": \"/maintenance\", \"not\": true }\n[ cond1, cond2 ]  // implicit AND\n```\n\n## Built-in Actions\n\n`setState`, `pushState`, `removeState`, and `validateForm` are built into the Vue schema and handled by `ActionProvider`:\n\n```json\n{\n  \"action\": \"setState\",\n  \"params\": { \"statePath\": \"/activeTab\", \"value\": \"settings\" }\n}\n```\n\n## Event System\n\nComponents use `emit(event)` to fire events, or `on(event)` for metadata (`shouldPreventDefault`, `bound`).\n\n## Streaming\n\n`useUIStream` and `useChatUI` return Vue Refs for streaming specs from an API.\n\n## BaseComponentProps\n\nFor catalog-agnostic reusable components:\n\n```typescript\nimport type { BaseComponentProps } from \"@json-render/vue\";\n\nconst Card = ({ props, children }: BaseComponentProps<{ title?: string }>) =>\n  h(\"div\", null, [props.title, children]);\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| `schema` | Element tree schema (from `@json-render/vue/schema`) |\n| `StateProvider`, `ActionProvider`, `VisibilityProvider`, `ValidationProvider` | Context providers |\n| `useStateStore`, `useStateValue`, `useBoundProp` | State composables |\n| `useActions`, `useAction` | Action composables |\n| `useFieldValidation`, `useIsVisible` | Validation and visibility |\n| `useUIStream`, `useChatUI` | Streaming composables |\n| `createStateStore` | Create in-memory `StateStore` |\n| `BaseComponentProps` | Catalog-agnostic component props type |\n"
  },
  {
    "path": "skills/xstate/SKILL.md",
    "content": "---\nname: xstate\ndescription: XState Store adapter for json-render's StateStore interface. Use when integrating json-render with @xstate/store for state management via @json-render/xstate.\n---\n\n# @json-render/xstate\n\n[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface. Wire an `@xstate/store` atom as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/xstate @json-render/core @json-render/react @xstate/store\n```\n\nRequires `@xstate/store` v3+.\n\n## Usage\n\n```tsx\nimport { createAtom } from \"@xstate/store\";\nimport { xstateStoreStateStore } from \"@json-render/xstate\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create an atom\nconst uiAtom = createAtom({ count: 0 });\n\n// 2. Create the json-render StateStore adapter\nconst store = xstateStoreStateStore({ atom: uiAtom });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through @xstate/store */}\n</StateProvider>\n```\n\n## API\n\n### `xstateStoreStateStore(options)`\n\nCreates a `StateStore` backed by an `@xstate/store` atom.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `atom` | `Atom<StateModel>` | Yes | An `@xstate/store` atom (from `createAtom`) holding the json-render state model |\n"
  },
  {
    "path": "skills/yaml/SKILL.md",
    "content": "---\nname: yaml\ndescription: 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.\n---\n\n# @json-render/yaml\n\nYAML wire format for `@json-render/core`. Progressive rendering and surgical edits via streaming YAML.\n\n## Key Concepts\n\n- **YAML wire format**: Alternative to JSONL that uses code fences (`yaml-spec`, `yaml-edit`, `yaml-patch`, `diff`)\n- **Streaming parser**: Incrementally parses YAML, emits JSON Patch operations via diffing\n- **Edit modes**: Patch (RFC 6902), merge (RFC 7396), and unified diff\n- **AI SDK transform**: `TransformStream` that converts YAML fences into json-render patches\n\n## Generating YAML Prompts\n\n```typescript\nimport { yamlPrompt } from \"@json-render/yaml\";\nimport { catalog } from \"./catalog\";\n\n// Standalone mode (LLM outputs only YAML)\nconst systemPrompt = yamlPrompt(catalog, {\n  mode: \"standalone\",\n  editModes: [\"merge\"],\n  customRules: [\"Always use dark theme\"],\n});\n\n// Inline mode (LLM responds conversationally, wraps YAML in fences)\nconst chatPrompt = yamlPrompt(catalog, { mode: \"inline\" });\n```\n\nOptions:\n\n- `system` (string) — Custom system message intro\n- `mode` (\"standalone\" | \"inline\") — Output mode, default \"standalone\"\n- `customRules` (string[]) — Additional rules appended to prompt\n- `editModes` (EditMode[]) — Edit modes to document, default [\"merge\"]\n\n## AI SDK Transform\n\nUse `pipeYamlRender` as a drop-in replacement for `pipeJsonRender`:\n\n```typescript\nimport { pipeYamlRender } from \"@json-render/yaml\";\nimport { createUIMessageStream, createUIMessageStreamResponse } from \"ai\";\n\nconst stream = createUIMessageStream({\n  execute: async ({ writer }) => {\n    writer.merge(pipeYamlRender(result.toUIMessageStream()));\n  },\n});\nreturn createUIMessageStreamResponse({ stream });\n```\n\nFor multi-turn edits, pass the previous spec:\n\n```typescript\npipeYamlRender(result.toUIMessageStream(), {\n  previousSpec: currentSpec,\n});\n```\n\nThe transform recognizes four fence types:\n\n- `yaml-spec` — Full spec, parsed progressively line-by-line\n- `yaml-edit` — Partial YAML deep-merged with current spec (RFC 7396)\n- `yaml-patch` — RFC 6902 JSON Patch lines\n- `diff` — Unified diff applied to serialized spec\n\n## Streaming Parser (Low-Level)\n\n```typescript\nimport { createYamlStreamCompiler } from \"@json-render/yaml\";\n\nconst compiler = createYamlStreamCompiler<Spec>();\n\n// Feed chunks as they arrive from any source\nconst { result, newPatches } = compiler.push(\"root: main\\n\");\ncompiler.push(\"elements:\\n  main:\\n    type: Card\\n\");\n\n// Flush remaining data at end of stream\nconst { result: final } = compiler.flush();\n\n// Reset for next stream (optionally with initial state)\ncompiler.reset({ root: \"main\", elements: {} });\n```\n\nMethods: `push(chunk)`, `flush()`, `getResult()`, `getPatches()`, `reset(initial?)`\n\n## Edit Modes (from @json-render/core)\n\nThe YAML package uses the universal edit mode system from core:\n\n```typescript\nimport { buildEditInstructions, buildEditUserPrompt } from \"@json-render/core\";\nimport type { EditMode } from \"@json-render/core\";\n\n// Generate edit instructions for YAML format\nconst instructions = buildEditInstructions({ modes: [\"merge\", \"patch\"] }, \"yaml\");\n\n// Build user prompt with current spec context\nconst userPrompt = buildEditUserPrompt({\n  prompt: \"Change the title to Dashboard\",\n  currentSpec: spec,\n  config: { modes: [\"merge\"] },\n  format: \"yaml\",\n  serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),\n});\n```\n\n## Fence Constants\n\nFor custom parsing, use the exported constants:\n\n```typescript\nimport {\n  YAML_SPEC_FENCE,   // \"```yaml-spec\"\n  YAML_EDIT_FENCE,   // \"```yaml-edit\"\n  YAML_PATCH_FENCE,  // \"```yaml-patch\"\n  DIFF_FENCE,        // \"```diff\"\n  FENCE_CLOSE,       // \"```\"\n} from \"@json-render/yaml\";\n```\n\n## Key Exports\n\n| Export | Description |\n|--------|-------------|\n| `yamlPrompt` | Generate YAML system prompt from catalog |\n| `createYamlTransform` | AI SDK TransformStream for YAML fences |\n| `pipeYamlRender` | Convenience pipe wrapper (replaces `pipeJsonRender`) |\n| `createYamlStreamCompiler` | Streaming YAML parser with patch emission |\n| `YAML_SPEC_FENCE` | Fence constant for yaml-spec |\n| `YAML_EDIT_FENCE` | Fence constant for yaml-edit |\n| `YAML_PATCH_FENCE` | Fence constant for yaml-patch |\n| `DIFF_FENCE` | Fence constant for diff |\n| `FENCE_CLOSE` | Fence close constant |\n| `diffToPatches` | Re-export: object diff to JSON Patch |\n| `deepMergeSpec` | Re-export: RFC 7396 deep merge |\n"
  },
  {
    "path": "skills/zustand/SKILL.md",
    "content": "---\nname: zustand\ndescription: Zustand adapter for json-render's StateStore interface. Use when integrating json-render with Zustand for state management via @json-render/zustand.\n---\n\n# @json-render/zustand\n\nZustand adapter for json-render's `StateStore` interface. Wire a Zustand vanilla store as the state backend for json-render.\n\n## Installation\n\n```bash\nnpm install @json-render/zustand @json-render/core @json-render/react zustand\n```\n\nRequires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.\n\n## Usage\n\n```tsx\nimport { createStore } from \"zustand/vanilla\";\nimport { zustandStateStore } from \"@json-render/zustand\";\nimport { StateProvider } from \"@json-render/react\";\n\n// 1. Create a Zustand vanilla store\nconst bearStore = createStore(() => ({\n  count: 0,\n  name: \"Bear\",\n}));\n\n// 2. Create the json-render StateStore adapter\nconst store = zustandStateStore({ store: bearStore });\n\n// 3. Use it\n<StateProvider store={store}>\n  {/* json-render reads/writes go through Zustand */}\n</StateProvider>\n```\n\n### With a Nested Slice\n\n```tsx\nconst appStore = createStore(() => ({\n  ui: { count: 0 },\n  auth: { token: null },\n}));\n\nconst store = zustandStateStore({\n  store: appStore,\n  selector: (s) => s.ui,\n  updater: (next, s) => s.setState({ ui: next }),\n});\n```\n\n## API\n\n### `zustandStateStore(options)`\n\nCreates a `StateStore` backed by a Zustand store.\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `store` | `StoreApi<S>` | Yes | Zustand vanilla store (from `createStore` in `zustand/vanilla`) |\n| `selector` | `(state) => StateModel` | No | Select the json-render slice. Defaults to entire state. |\n| `updater` | `(nextState, store) => void` | No | Apply next state to the store. Defaults to shallow merge. Override for nested slices, or use `(next, s) => s.setState(next, true)` for full replacement. |\n"
  },
  {
    "path": "tests/e2e/package.json",
    "content": "{\n  \"name\": \"e2e-tests\",\n  \"private\": true,\n  \"scripts\": {\n    \"test\": \"vitest run --config vitest.config.ts\"\n  },\n  \"devDependencies\": {\n    \"@ai-sdk/gateway\": \"^3.0.53\",\n    \"@json-render/core\": \"workspace:*\",\n    \"@json-render/jotai\": \"workspace:*\",\n    \"@json-render/react\": \"workspace:*\",\n    \"@json-render/redux\": \"workspace:*\",\n    \"@json-render/zustand\": \"workspace:*\",\n    \"@reduxjs/toolkit\": \"^2.11.2\",\n    \"ai\": \"^6.0.97\",\n    \"jotai\": \"^2.18.0\",\n    \"redux\": \"^5.0.1\",\n    \"vitest\": \"^4.0.17\",\n    \"zod\": \"^4.3.6\",\n    \"zustand\": \"^5.0.11\"\n  }\n}\n"
  },
  {
    "path": "tests/e2e/state-store-e2e.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { streamText } from \"ai\";\nimport { z } from \"zod\";\nimport {\n  defineCatalog,\n  buildUserPrompt,\n  createStateStore,\n  createSpecStreamCompiler,\n  type Spec,\n  type StateStore,\n} from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { reduxStateStore } from \"@json-render/redux\";\nimport { zustandStateStore } from \"@json-render/zustand\";\nimport { jotaiStateStore } from \"@json-render/jotai\";\nimport { configureStore, createSlice } from \"@reduxjs/toolkit\";\nimport { createStore as createZustandStore } from \"zustand/vanilla\";\nimport { atom } from \"jotai\";\nimport { createStore as createJotaiStore } from \"jotai/vanilla\";\n\nconst HAS_API_KEY = !!process.env.AI_GATEWAY_API_KEY;\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    Text: {\n      props: z.object({ content: z.string() }),\n      description: \"Text content\",\n    },\n    Input: {\n      props: z.object({\n        label: z.string().nullable(),\n        value: z.string().nullable(),\n        placeholder: z.string().nullable(),\n      }),\n      description:\n        \"Text input. Use value with $bindState for two-way state binding.\",\n      example: {\n        label: \"Name\",\n        value: { $bindState: \"/form/name\" },\n        placeholder: \"Enter name\",\n      },\n    },\n    Button: {\n      props: z.object({\n        label: z.string(),\n        action: z.string().nullable(),\n      }),\n      description: \"Clickable button\",\n    },\n    Stack: {\n      props: z.object({\n        direction: z.enum([\"horizontal\", \"vertical\"]).nullable(),\n      }),\n      slots: [\"default\"],\n      description: \"Layout container\",\n    },\n  },\n  actions: {},\n});\n\nasync function generateSpec(): Promise<Spec> {\n  const prompt = buildUserPrompt({\n    prompt:\n      \"Create a simple form with two text inputs bound to state: one for /form/name and one for /form/email. Include initial state with name set to 'Alice' and email set to 'alice@example.com'. Use a vertical Stack as the container.\",\n  });\n\n  const result = streamText({\n    model: \"anthropic/claude-haiku-4.5\",\n    system: catalog.prompt(),\n    prompt,\n    temperature: 0,\n  });\n\n  const compiler = createSpecStreamCompiler<Spec>();\n\n  for await (const chunk of result.textStream) {\n    compiler.push(chunk);\n  }\n\n  return compiler.getResult();\n}\n\nfunction extractStatePaths(spec: Spec): string[] {\n  const paths: string[] = [];\n\n  function walk(obj: unknown) {\n    if (obj === null || obj === undefined) return;\n    if (typeof obj !== \"object\") return;\n    if (Array.isArray(obj)) {\n      for (const item of obj) walk(item);\n      return;\n    }\n    const record = obj as Record<string, unknown>;\n    if (\"$bindState\" in record && typeof record.$bindState === \"string\") {\n      paths.push(record.$bindState);\n    }\n    if (\"$state\" in record && typeof record.$state === \"string\") {\n      paths.push(record.$state);\n    }\n    for (const value of Object.values(record)) {\n      walk(value);\n    }\n  }\n\n  if (spec.elements) walk(spec.elements);\n  return [...new Set(paths)];\n}\n\nfunction flattenState(\n  obj: Record<string, unknown>,\n  prefix = \"\",\n): Record<string, unknown> {\n  const result: Record<string, unknown> = {};\n  for (const [key, value] of Object.entries(obj)) {\n    const pointer = `${prefix}/${key}`;\n    if (\n      value !== null &&\n      typeof value === \"object\" &&\n      !Array.isArray(value) &&\n      Object.getPrototypeOf(value) === Object.prototype\n    ) {\n      Object.assign(\n        result,\n        flattenState(value as Record<string, unknown>, pointer),\n      );\n    } else {\n      result[pointer] = value;\n    }\n  }\n  return result;\n}\n\nfunction verifyStoreRoundTrip(store: StateStore, spec: Spec) {\n  if (spec.state) {\n    const flat = flattenState(spec.state);\n    store.update(flat);\n  }\n\n  const paths = extractStatePaths(spec);\n  expect(paths.length).toBeGreaterThan(0);\n\n  for (const path of paths) {\n    const original = store.get(path);\n    const testValue = `test-${Date.now()}`;\n\n    store.set(path, testValue);\n    expect(store.get(path)).toBe(testValue);\n\n    store.set(path, original);\n    expect(store.get(path)).toBe(original);\n  }\n\n  const batchUpdate: Record<string, unknown> = {};\n  for (const path of paths) {\n    batchUpdate[path] = `batch-${Date.now()}`;\n  }\n  store.update(batchUpdate);\n  for (const path of paths) {\n    expect(store.get(path)).toBe(batchUpdate[path]);\n  }\n}\n\ndescribe.skipIf(!HAS_API_KEY)(\n  \"e2e: Claude Haiku 4.5 + StateStore adapters\",\n  () => {\n    let spec: Spec;\n\n    it(\"generates a valid spec with state bindings from Claude Haiku 4.5\", async () => {\n      spec = await generateSpec();\n\n      expect(spec.root).toBeTruthy();\n      expect(spec.elements).toBeTruthy();\n      expect(Object.keys(spec.elements).length).toBeGreaterThan(0);\n\n      const paths = extractStatePaths(spec);\n      expect(paths.length).toBeGreaterThan(0);\n    });\n\n    it(\"works with createStateStore (built-in)\", () => {\n      const store = createStateStore(spec.state ?? {});\n      verifyStoreRoundTrip(store, spec);\n    });\n\n    it(\"works with reduxStateStore (Redux Toolkit)\", () => {\n      const uiSlice = createSlice({\n        name: \"ui\",\n        initialState: (spec.state ?? {}) as Record<string, unknown>,\n        reducers: {\n          replace: (_state, action) => action.payload,\n        },\n      });\n\n      const reduxStore = configureStore({\n        reducer: { ui: uiSlice.reducer },\n      });\n\n      const store = reduxStateStore({\n        store: reduxStore,\n        selector: (state) => state.ui as Record<string, unknown>,\n        dispatch: (next, s) => s.dispatch(uiSlice.actions.replace(next)),\n      });\n\n      verifyStoreRoundTrip(store, spec);\n    });\n\n    it(\"works with zustandStateStore (Zustand)\", () => {\n      const zStore = createZustandStore<Record<string, unknown>>()(\n        () => spec.state ?? {},\n      );\n      const store = zustandStateStore({ store: zStore });\n      verifyStoreRoundTrip(store, spec);\n    });\n\n    it(\"works with jotaiStateStore (Jotai)\", () => {\n      const stateAtom = atom<Record<string, unknown>>(spec.state ?? {});\n      const jStore = createJotaiStore();\n      const store = jotaiStateStore({ atom: stateAtom, store: jStore });\n      verifyStoreRoundTrip(store, spec);\n    });\n  },\n);\n"
  },
  {
    "path": "tests/e2e/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"path\";\n\nconst root = path.resolve(__dirname, \"../..\");\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@json-render/core/store-utils\": path.resolve(\n        root,\n        \"packages/core/src/store-utils.ts\",\n      ),\n      \"@json-render/core\": path.resolve(root, \"packages/core/src/index.ts\"),\n      \"@json-render/react/schema\": path.resolve(\n        root,\n        \"packages/react/src/schema.ts\",\n      ),\n      \"@json-render/redux\": path.resolve(root, \"packages/redux/src/index.ts\"),\n      \"@json-render/zustand\": path.resolve(\n        root,\n        \"packages/zustand/src/index.ts\",\n      ),\n      \"@json-render/jotai\": path.resolve(root, \"packages/jotai/src/index.ts\"),\n    },\n  },\n  test: {\n    globals: true,\n    environment: \"node\",\n    include: [\"**/*.test.ts\"],\n    testTimeout: 60_000,\n  },\n});\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turborepo.dev/schema.json\",\n  \"ui\": \"tui\",\n  \"globalEnv\": [\"AI_GATEWAY_MODEL\", \"KV_REST_API_URL\", \"KV_REST_API_TOKEN\", \"RATE_LIMIT_PER_MINUTE\", \"RATE_LIMIT_PER_DAY\"],\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"inputs\": [\"$TURBO_DEFAULT$\", \".env*\"],\n      \"outputs\": [\".next/**\", \"!.next/cache/**\", \"dist/**\"]\n    },\n    \"lint\": {\n      \"dependsOn\": [\"^lint\"]\n    },\n    \"check-types\": {\n      \"dependsOn\": [\"^build\"]\n    },\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    }\n  }\n}\n"
  },
  {
    "path": "vitest.config.mts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { svelte } from \"@sveltejs/vite-plugin-svelte\";\nimport solid from \"vite-plugin-solid\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineConfig({\n  plugins: [\n    svelte({ hot: false }),\n    solid({\n      // Only transform files in the solid package to avoid interfering with React JSX\n      include: [\"packages/solid/**/*.{ts,tsx}\"],\n    }),\n  ],\n  resolve: {\n    // Ensure Svelte resolves to browser bundle, not server\n    conditions: [\"browser\"],\n    // Deduplicate React, Vue, and Solid so tests don't get two copies\n    // (pnpm strict resolution can cause packages to resolve different copies)\n    alias: {\n      react: path.resolve(__dirname, \"node_modules/react\"),\n      \"react-dom\": path.resolve(__dirname, \"node_modules/react-dom\"),\n      vue: path.resolve(__dirname, \"packages/vue/node_modules/vue\"),\n      \"solid-js\": path.resolve(__dirname, \"node_modules/solid-js\"),\n    },\n  },\n  test: {\n    globals: true,\n    environment: \"jsdom\",\n    include: [\"packages/**/*.test.ts\", \"packages/**/*.test.tsx\"],\n    coverage: {\n      provider: \"v8\",\n      reporter: [\"text\", \"json\", \"html\"],\n      include: [\"packages/*/src/**/*.{ts,tsx,svelte}\"],\n      exclude: [\"**/*.test.{ts,tsx}\", \"**/index.ts\"],\n    },\n  },\n});\n"
  }
]