Переглянути джерело

next (#246)

* next

* next example

* component catalog

* fixes

* fixes
Chris Tate 3 місяців тому
батько
коміт
a99d68c481
52 змінених файлів з 4141 додано та 26 видалено
  1. 2 1
      .changeset/config.json
  2. 3 0
      README.md
  3. 280 0
      apps/web/app/(main)/docs/api/next/page.mdx
  4. 2 2
      apps/web/app/api/docs-chat/route.ts
  5. 1 0
      apps/web/lib/docs-navigation.ts
  6. 1 0
      apps/web/lib/page-titles.ts
  7. 12 0
      examples/next-website-builder/app/[[...slug]]/layout.tsx
  8. 15 0
      examples/next-website-builder/app/[[...slug]]/page.tsx
  9. 7 0
      examples/next-website-builder/app/[[...slug]]/renderer.tsx
  10. 11 0
      examples/next-website-builder/app/api/spec/route.ts
  11. 5 0
      examples/next-website-builder/app/builder/page.tsx
  12. 101 0
      examples/next-website-builder/app/globals.css
  13. 23 0
      examples/next-website-builder/app/layout.tsx
  14. 180 0
      examples/next-website-builder/components/editor.tsx
  15. 71 0
      examples/next-website-builder/components/route-tabs.tsx
  16. 53 0
      examples/next-website-builder/components/ui/resizable.tsx
  17. 7 0
      examples/next-website-builder/lib/app.ts
  18. 12 0
      examples/next-website-builder/lib/catalog.ts
  19. 307 0
      examples/next-website-builder/lib/default-spec.ts
  20. 14 0
      examples/next-website-builder/lib/registry.tsx
  21. 12 0
      examples/next-website-builder/lib/spec-store.ts
  22. 6 0
      examples/next-website-builder/lib/utils.ts
  23. 188 0
      examples/next-website-builder/lib/website-catalog.ts
  24. 501 0
      examples/next-website-builder/lib/website-components.tsx
  25. 6 0
      examples/next-website-builder/next-env.d.ts
  26. 12 0
      examples/next-website-builder/next.config.ts
  27. 41 0
      examples/next-website-builder/package.json
  28. 5 0
      examples/next-website-builder/postcss.config.mjs
  29. 13 0
      examples/next-website-builder/tsconfig.json
  30. 103 0
      packages/next/README.md
  31. 71 0
      packages/next/package.json
  32. 17 0
      packages/next/src/catalog-types.ts
  33. 69 0
      packages/next/src/components/error-boundary.tsx
  34. 52 0
      packages/next/src/components/link.tsx
  35. 45 0
      packages/next/src/components/loading-renderer.tsx
  36. 27 0
      packages/next/src/components/not-found-renderer.tsx
  37. 135 0
      packages/next/src/components/page-renderer.tsx
  38. 92 0
      packages/next/src/components/provider.tsx
  39. 148 0
      packages/next/src/create-app.ts
  40. 73 0
      packages/next/src/index.ts
  41. 101 0
      packages/next/src/metadata.ts
  42. 4 0
      packages/next/src/page-renderer-client.tsx
  43. 196 0
      packages/next/src/router.ts
  44. 332 0
      packages/next/src/schema.ts
  45. 55 0
      packages/next/src/server.ts
  46. 176 0
      packages/next/src/types.ts
  47. 9 0
      packages/next/tsconfig.json
  48. 35 0
      packages/next/tsup.config.ts
  49. 5 2
      packages/shadcn/src/catalog.ts
  50. 16 3
      packages/shadcn/src/components.tsx
  51. 295 18
      pnpm-lock.yaml
  52. 194 0
      skills/next/SKILL.md

+ 2 - 1
.changeset/config.json

@@ -24,7 +24,8 @@
       "@json-render/react-three-fiber",
       "@json-render/yaml",
       "@json-render/shadcn-svelte",
-      "@json-render/ink"
+      "@json-render/ink",
+      "@json-render/next"
     ]
   ],
   "linked": [],

+ 3 - 0
README.md

@@ -25,6 +25,8 @@ npm install @json-render/core @json-render/svelte
 npm install @json-render/core @json-render/solid
 # or for terminal UIs
 npm install @json-render/core @json-render/ink ink react
+# or for full Next.js apps (routes, layouts, SSR, metadata)
+npm install @json-render/core @json-render/react @json-render/next
 # or for 3D scenes
 npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
 ```
@@ -127,6 +129,7 @@ function Dashboard({ spec }) {
 | `@json-render/shadcn`       | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS)            |
 | `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components)  |
 | `@json-render/react-native` | React Native renderer with standard mobile components                  |
+| `@json-render/next`         | Next.js renderer — JSON becomes full apps with routes, layouts, SSR    |
 | `@json-render/remotion`     | Remotion video renderer, timeline schema                               |
 | `@json-render/react-pdf`    | React PDF renderer for generating PDF documents from specs             |
 | `@json-render/react-email`  | React Email renderer for HTML/plain-text emails from specs             |

+ 280 - 0
apps/web/app/(main)/docs/api/next/page.mdx

@@ -0,0 +1,280 @@
+import { pageMetadata } from "@/lib/page-metadata"
+export const metadata = pageMetadata("docs/api/next")
+
+# @json-render/next
+
+Next.js renderer. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
+
+## Installation
+
+```bash
+npm install @json-render/core @json-render/react @json-render/next
+```
+
+## schema
+
+The Next.js app schema for multi-page specs. Use with `defineCatalog` from core.
+
+```typescript
+import { defineCatalog } from '@json-render/core';
+import { schema } from '@json-render/next/server';
+import { z } from 'zod';
+
+const catalog = defineCatalog(schema, {
+  components: {
+    Card: {
+      props: z.object({ title: z.string() }),
+      description: 'Card container',
+    },
+    NavBar: {
+      props: z.object({ links: z.array(z.object({ href: z.string(), label: z.string() })) }),
+      description: 'Navigation bar',
+    },
+  },
+  actions: {},
+});
+```
+
+## createNextApp
+
+Create all exports needed for a Next.js `[[...slug]]` catch-all route.
+
+```typescript
+import { createNextApp } from '@json-render/next/server';
+
+const { Page, generateMetadata, generateStaticParams } = createNextApp({
+  spec: myAppSpec,
+  loaders: {
+    loadPost: async ({ slug }) => {
+      const post = await db.post.findUnique({ where: { slug } });
+      return { post };
+    },
+  },
+});
+```
+
+### Options
+
+<table>
+  <thead>
+    <tr>
+      <th>Option</th>
+      <th>Type</th>
+      <th>Description</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>spec</code></td>
+      <td><code>{'NextAppSpec | (() => NextAppSpec | Promise<NextAppSpec>)'}</code></td>
+      <td>The application spec (static or dynamic)</td>
+    </tr>
+    <tr>
+      <td><code>loaders</code></td>
+      <td><code>{'Record<string, LoaderFn>'}</code></td>
+      <td>Server-side data loaders keyed by name</td>
+    </tr>
+  </tbody>
+</table>
+
+### Returns
+
+<table>
+  <thead>
+    <tr>
+      <th>Export</th>
+      <th>Description</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>Page</code></td>
+      <td>Async Server Component for <code>page.tsx</code></td>
+    </tr>
+    <tr>
+      <td><code>generateMetadata</code></td>
+      <td>Metadata generator for Next.js SEO</td>
+    </tr>
+    <tr>
+      <td><code>generateStaticParams</code></td>
+      <td>Static params for pre-rendering at build time</td>
+    </tr>
+  </tbody>
+</table>
+
+## NextAppSpec
+
+The top-level spec defining an entire Next.js application.
+
+```typescript
+interface NextAppSpec {
+  metadata?: NextMetadata;
+  routes: Record<string, NextRouteSpec>;
+  layouts?: Record<string, Spec>;
+  state?: Record<string, unknown>;
+}
+```
+
+### Route Patterns
+
+Routes use Next.js URL conventions:
+
+<table>
+  <thead>
+    <tr>
+      <th>Pattern</th>
+      <th>Example Match</th>
+      <th>Params</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>/</code></td>
+      <td><code>/</code></td>
+      <td><code>{'{}'}</code></td>
+    </tr>
+    <tr>
+      <td><code>/about</code></td>
+      <td><code>/about</code></td>
+      <td><code>{'{}'}</code></td>
+    </tr>
+    <tr>
+      <td><code>/blog/[slug]</code></td>
+      <td><code>/blog/hello</code></td>
+      <td><code>{'{ slug: "hello" }'}</code></td>
+    </tr>
+    <tr>
+      <td><code>/docs/[...path]</code></td>
+      <td><code>/docs/a/b/c</code></td>
+      <td><code>{'{ path: ["a","b","c"] }'}</code></td>
+    </tr>
+    <tr>
+      <td><code>/app/[[...path]]</code></td>
+      <td><code>/app</code> or <code>/app/x/y</code></td>
+      <td><code>{'{ path: [] }'}</code> or <code>{'{ path: ["x","y"] }'}</code></td>
+    </tr>
+  </tbody>
+</table>
+
+## NextAppProvider
+
+Client component that provides the component registry and action handlers to all pages.
+
+```tsx
+import { NextAppProvider } from '@json-render/next';
+
+export default function Layout({ children }) {
+  return (
+    <NextAppProvider registry={registry} handlers={handlers}>
+      {children}
+    </NextAppProvider>
+  );
+}
+```
+
+## Built-in Components
+
+### Slot
+
+Placeholder in layouts where page content is rendered. Every layout MUST include a Slot.
+
+```json
+{ "type": "Slot", "props": {}, "children": [] }
+```
+
+### Link
+
+Client-side navigation wrapping `next/link`.
+
+```json
+{ "type": "Link", "props": { "href": "/about" }, "children": ["link-text"] }
+```
+
+## Built-in Actions
+
+<table>
+  <thead>
+    <tr>
+      <th>Action</th>
+      <th>Params</th>
+      <th>Description</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>setState</code></td>
+      <td><code>{'{ statePath, value }'}</code></td>
+      <td>Update a value in state</td>
+    </tr>
+    <tr>
+      <td><code>pushState</code></td>
+      <td><code>{'{ statePath, value, clearStatePath? }'}</code></td>
+      <td>Append to array in state</td>
+    </tr>
+    <tr>
+      <td><code>removeState</code></td>
+      <td><code>{'{ statePath, index }'}</code></td>
+      <td>Remove from array by index</td>
+    </tr>
+    <tr>
+      <td><code>navigate</code></td>
+      <td><code>{'{ href }'}</code></td>
+      <td>Client-side navigation</td>
+    </tr>
+  </tbody>
+</table>
+
+## Server Utilities
+
+### matchRoute
+
+Match a pathname against a spec's routes.
+
+```typescript
+import { matchRoute } from '@json-render/next/server';
+
+const matched = matchRoute(spec, '/blog/hello-world');
+// { route: NextRouteSpec, pattern: '/blog/[slug]', params: { slug: 'hello-world' } }
+```
+
+### resolveMetadata
+
+Resolve merged metadata for a route.
+
+```typescript
+import { resolveMetadata } from '@json-render/next/server';
+
+const metadata = resolveMetadata(spec, matchedRoute?.route);
+```
+
+### slugToPath
+
+Convert catch-all slug array to pathname.
+
+```typescript
+import { slugToPath } from '@json-render/next/server';
+
+slugToPath(undefined);          // "/"
+slugToPath(['blog', 'hello']);  // "/blog/hello"
+```
+
+## Entry Points
+
+<table>
+  <thead>
+    <tr>
+      <th>Import</th>
+      <th>Contents</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td><code>@json-render/next</code></td>
+      <td>Client components (NextAppProvider, PageRenderer, Link)</td>
+    </tr>
+    <tr>
+      <td><code>@json-render/next/server</code></td>
+      <td>Server utilities (createNextApp, matchRoute, schema)</td>
+    </tr>
+  </tbody>
+</table>

+ 2 - 2
apps/web/app/api/docs-chat/route.ts

@@ -16,8 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
 
 GitHub repository: https://github.com/vercel-labs/json-render
 Documentation: https://json-render.dev/docs
-npm packages: @json-render/core, @json-render/react, @json-render/ink, @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
-Skills: 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, ink, 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.
+npm packages: @json-render/core, @json-render/react, @json-render/next, @json-render/ink, @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
+Skills: 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, next, ink, 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.
 
 You 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.
 

+ 1 - 0
apps/web/lib/docs-navigation.ts

@@ -69,6 +69,7 @@ export const docsNavigation: NavSection[] = [
     items: [
       { title: "@json-render/core", href: "/docs/api/core" },
       { title: "@json-render/react", href: "/docs/api/react" },
+      { title: "@json-render/next", href: "/docs/api/next" },
       { title: "@json-render/react-pdf", href: "/docs/api/react-pdf" },
       { title: "@json-render/react-email", href: "/docs/api/react-email" },
       { title: "@json-render/shadcn", href: "/docs/api/shadcn" },

+ 1 - 0
apps/web/lib/page-titles.ts

@@ -43,6 +43,7 @@ export const PAGE_TITLES: Record<string, string> = {
   // API references
   "docs/api/core": "@json-render/core API",
   "docs/api/react": "@json-render/react API",
+  "docs/api/next": "@json-render/next API",
   "docs/api/vue": "@json-render/vue API",
   "docs/api/solid": "@json-render/solid API",
   "docs/api/react-pdf": "@json-render/react-pdf API",

+ 12 - 0
examples/next-website-builder/app/[[...slug]]/layout.tsx

@@ -0,0 +1,12 @@
+"use client";
+
+import { NextAppProvider } from "@json-render/next";
+import { registry } from "@/lib/registry";
+
+export default function WebsiteLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  return <NextAppProvider registry={registry}>{children}</NextAppProvider>;
+}

+ 15 - 0
examples/next-website-builder/app/[[...slug]]/page.tsx

@@ -0,0 +1,15 @@
+import { notFound } from "next/navigation";
+import { getPageData, generateMetadata, generateStaticParams } from "@/lib/app";
+import { WebsiteRenderer } from "./renderer";
+
+export { generateMetadata, generateStaticParams };
+
+export default async function Page({
+  params,
+}: {
+  params: Promise<{ slug?: string[] }>;
+}) {
+  const data = await getPageData({ params });
+  if (!data) notFound();
+  return <WebsiteRenderer {...data} />;
+}

+ 7 - 0
examples/next-website-builder/app/[[...slug]]/renderer.tsx

@@ -0,0 +1,7 @@
+"use client";
+
+import { PageRenderer, type PageRendererProps } from "@json-render/next";
+
+export function WebsiteRenderer(props: PageRendererProps) {
+  return <PageRenderer {...props} />;
+}

+ 11 - 0
examples/next-website-builder/app/api/spec/route.ts

@@ -0,0 +1,11 @@
+import type { NextAppSpec } from "@json-render/next";
+import { getSpec, setSpec } from "@/lib/spec-store";
+
+export async function GET() {
+  return Response.json(getSpec());
+}
+
+export async function PUT(req: Request) {
+  setSpec((await req.json()) as NextAppSpec);
+  return Response.json({ ok: true });
+}

+ 5 - 0
examples/next-website-builder/app/builder/page.tsx

@@ -0,0 +1,5 @@
+import { Editor } from "@/components/editor";
+
+export default function BuilderPage() {
+  return <Editor />;
+}

+ 101 - 0
examples/next-website-builder/app/globals.css

@@ -0,0 +1,101 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@source "../../../packages/shadcn/src/**/*.tsx";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+    --radius-sm: calc(var(--radius) - 4px);
+    --radius-md: calc(var(--radius) - 2px);
+    --radius-lg: var(--radius);
+    --radius-xl: calc(var(--radius) + 4px);
+    --radius-2xl: calc(var(--radius) + 8px);
+    --radius-3xl: calc(var(--radius) + 12px);
+    --radius-4xl: calc(var(--radius) + 16px);
+    --color-background: var(--background);
+    --color-foreground: var(--foreground);
+    --color-card: var(--card);
+    --color-card-foreground: var(--card-foreground);
+    --color-popover: var(--popover);
+    --color-popover-foreground: var(--popover-foreground);
+    --color-primary: var(--primary);
+    --color-primary-foreground: var(--primary-foreground);
+    --color-secondary: var(--secondary);
+    --color-secondary-foreground: var(--secondary-foreground);
+    --color-muted: var(--muted);
+    --color-muted-foreground: var(--muted-foreground);
+    --color-accent: var(--accent);
+    --color-accent-foreground: var(--accent-foreground);
+    --color-destructive: var(--destructive);
+    --color-border: var(--border);
+    --color-input: var(--input);
+    --color-ring: var(--ring);
+    --color-chart-1: var(--chart-1);
+    --color-chart-2: var(--chart-2);
+    --color-chart-3: var(--chart-3);
+    --color-chart-4: var(--chart-4);
+    --color-chart-5: var(--chart-5);
+}
+
+:root {
+    --radius: 0.625rem;
+    --background: oklch(1 0 0);
+    --foreground: oklch(0.145 0 0);
+    --card: oklch(1 0 0);
+    --card-foreground: oklch(0.145 0 0);
+    --popover: oklch(1 0 0);
+    --popover-foreground: oklch(0.145 0 0);
+    --primary: oklch(0.205 0 0);
+    --primary-foreground: oklch(0.985 0 0);
+    --secondary: oklch(0.97 0 0);
+    --secondary-foreground: oklch(0.205 0 0);
+    --muted: oklch(0.97 0 0);
+    --muted-foreground: oklch(0.556 0 0);
+    --accent: oklch(0.97 0 0);
+    --accent-foreground: oklch(0.205 0 0);
+    --destructive: oklch(0.577 0.245 27.325);
+    --border: oklch(0.922 0 0);
+    --input: oklch(0.922 0 0);
+    --ring: oklch(0.708 0 0);
+    --chart-1: oklch(0.646 0.222 41.116);
+    --chart-2: oklch(0.6 0.118 184.704);
+    --chart-3: oklch(0.398 0.07 227.392);
+    --chart-4: oklch(0.828 0.189 84.429);
+    --chart-5: oklch(0.769 0.188 70.08);
+}
+
+.dark {
+    --background: oklch(0.145 0 0);
+    --foreground: oklch(0.985 0 0);
+    --card: oklch(0.205 0 0);
+    --card-foreground: oklch(0.985 0 0);
+    --popover: oklch(0.205 0 0);
+    --popover-foreground: oklch(0.985 0 0);
+    --primary: oklch(0.922 0 0);
+    --primary-foreground: oklch(0.205 0 0);
+    --secondary: oklch(0.269 0 0);
+    --secondary-foreground: oklch(0.985 0 0);
+    --muted: oklch(0.269 0 0);
+    --muted-foreground: oklch(0.708 0 0);
+    --accent: oklch(0.269 0 0);
+    --accent-foreground: oklch(0.985 0 0);
+    --destructive: oklch(0.704 0.191 22.216);
+    --border: oklch(1 0 0 / 10%);
+    --input: oklch(1 0 0 / 15%);
+    --ring: oklch(0.556 0 0);
+    --chart-1: oklch(0.488 0.243 264.376);
+    --chart-2: oklch(0.696 0.17 162.48);
+    --chart-3: oklch(0.769 0.188 70.08);
+    --chart-4: oklch(0.627 0.265 303.9);
+    --chart-5: oklch(0.645 0.246 16.439);
+}
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
+}

+ 23 - 0
examples/next-website-builder/app/layout.tsx

@@ -0,0 +1,23 @@
+import type { Metadata } from "next";
+import { GeistSans } from "geist/font/sans";
+import { GeistMono } from "geist/font/mono";
+import "./globals.css";
+
+export const metadata: Metadata = {
+  title: "Next Website Builder | json-render",
+  description:
+    "Build entire Next.js websites from JSON specs with @json-render/next",
+  icons: { icon: "/icon.svg" },
+};
+
+export default function RootLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  return (
+    <html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
+      <body className="font-sans antialiased">{children}</body>
+    </html>
+  );
+}

+ 180 - 0
examples/next-website-builder/components/editor.tsx

@@ -0,0 +1,180 @@
+"use client";
+
+import {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+  type MouseEvent,
+} from "react";
+import { JsonEditor, type JsonValue } from "@visual-json/react";
+import type { NextAppSpec } from "@json-render/next";
+import { NextAppProvider, PageRenderer } from "@json-render/next";
+import {
+  ResizablePanelGroup,
+  ResizablePanel,
+  ResizableHandle,
+} from "@/components/ui/resizable";
+import { AddressBar } from "@/components/route-tabs";
+import { registry } from "@/lib/registry";
+
+export function Editor() {
+  const [spec, setSpec] = useState<NextAppSpec | null>(null);
+  const [activeRoute, setActiveRoute] = useState("/");
+  const [sidebarOpen, setSidebarOpen] = useState(true);
+  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  useEffect(() => {
+    fetch("/api/spec")
+      .then((r) => r.json())
+      .then((data: NextAppSpec) => setSpec(data));
+  }, []);
+
+  const handleChange = useCallback((value: JsonValue) => {
+    const updated = value as unknown as NextAppSpec;
+    setSpec(updated);
+
+    if (debounceRef.current) clearTimeout(debounceRef.current);
+    debounceRef.current = setTimeout(() => {
+      fetch("/api/spec", {
+        method: "PUT",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify(updated),
+      });
+    }, 500);
+  }, []);
+
+  const handlePreviewClick = useCallback((e: MouseEvent<HTMLDivElement>) => {
+    const anchor = (e.target as HTMLElement).closest("a");
+    if (!anchor) return;
+    const href = anchor.getAttribute("href");
+    if (!href || href.startsWith("http") || href.startsWith("mailto:")) return;
+    e.preventDefault();
+    setActiveRoute(href);
+  }, []);
+
+  const currentRoute = useMemo(() => {
+    if (!spec) return null;
+    return spec.routes[activeRoute] ?? null;
+  }, [spec, activeRoute]);
+
+  const layoutSpec = useMemo(() => {
+    if (!spec || !currentRoute?.layout || !spec.layouts) return null;
+    return spec.layouts[currentRoute.layout] ?? null;
+  }, [spec, currentRoute]);
+
+  const initialState = useMemo(() => {
+    if (!spec || !currentRoute) return undefined;
+    const merged: Record<string, unknown> = {};
+    if (spec.state) Object.assign(merged, spec.state);
+    if (currentRoute.page.state) Object.assign(merged, currentRoute.page.state);
+    return Object.keys(merged).length > 0 ? merged : undefined;
+  }, [spec, currentRoute]);
+
+  if (!spec) {
+    return (
+      <div className="flex items-center justify-center h-screen text-muted-foreground">
+        Loading...
+      </div>
+    );
+  }
+
+  return (
+    <div className="h-screen flex flex-col">
+      <div className="flex items-center justify-between px-4 h-12 border-b border-border bg-background shrink-0">
+        <span className="text-sm font-semibold">Next Website Builder</span>
+        <a
+          href="/"
+          target="_blank"
+          rel="noopener noreferrer"
+          className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+        >
+          View Website
+        </a>
+      </div>
+      <ResizablePanelGroup orientation="horizontal" className="flex-1">
+        <ResizablePanel defaultSize={45} minSize={25}>
+          <div className="h-full flex flex-col">
+            <div className="flex items-center justify-between px-3 h-10 border-b border-border bg-muted/30">
+              <span className="text-xs font-mono text-muted-foreground">
+                spec.json
+              </span>
+              <button
+                onClick={() => setSidebarOpen((v) => !v)}
+                className="flex items-center justify-center w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
+                title={sidebarOpen ? "Hide sidebar" : "Show sidebar"}
+              >
+                <svg
+                  width="14"
+                  height="14"
+                  viewBox="0 0 24 24"
+                  fill="none"
+                  stroke="currentColor"
+                  strokeWidth="2"
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                >
+                  <rect x="3" y="3" width="18" height="18" rx="2" />
+                  <path d="M9 3v18" />
+                </svg>
+              </button>
+            </div>
+            <div className="flex-1 overflow-auto">
+              <JsonEditor
+                value={spec as unknown as JsonValue}
+                onChange={handleChange}
+                sidebarOpen={sidebarOpen}
+                height="100%"
+                className="h-full"
+                style={
+                  {
+                    "--vj-bg": "var(--background)",
+                    "--vj-bg-panel": "var(--background)",
+                    "--vj-bg-hover": "var(--muted)",
+                    "--vj-bg-selected": "var(--primary)",
+                    "--vj-bg-selected-muted": "var(--muted)",
+                    "--vj-text": "var(--foreground)",
+                    "--vj-text-selected": "var(--primary-foreground)",
+                    "--vj-text-muted": "var(--muted-foreground)",
+                    "--vj-text-dim": "var(--muted-foreground)",
+                    "--vj-border": "var(--border)",
+                    "--vj-border-subtle": "var(--border)",
+                    "--vj-accent": "var(--primary)",
+                    "--vj-accent-muted": "var(--muted)",
+                    "--vj-input-bg": "var(--secondary)",
+                    "--vj-input-border": "var(--border)",
+                  } as React.CSSProperties
+                }
+              />
+            </div>
+          </div>
+        </ResizablePanel>
+        <ResizableHandle withHandle />
+        <ResizablePanel defaultSize={55} minSize={30}>
+          <div className="h-full flex flex-col">
+            <AddressBar route={activeRoute} onNavigate={setActiveRoute} />
+            <div
+              className="flex-1 overflow-auto bg-background"
+              onClick={handlePreviewClick}
+            >
+              {currentRoute ? (
+                <NextAppProvider registry={registry}>
+                  <PageRenderer
+                    spec={currentRoute.page}
+                    initialState={initialState}
+                    layoutSpec={layoutSpec}
+                  />
+                </NextAppProvider>
+              ) : (
+                <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
+                  Route not found
+                </div>
+              )}
+            </div>
+          </div>
+        </ResizablePanel>
+      </ResizablePanelGroup>
+    </div>
+  );
+}

+ 71 - 0
examples/next-website-builder/components/route-tabs.tsx

@@ -0,0 +1,71 @@
+"use client";
+
+import { useCallback, useState, type KeyboardEvent } from "react";
+
+interface AddressBarProps {
+  route: string;
+  onNavigate: (route: string) => void;
+}
+
+export function AddressBar({ route, onNavigate }: AddressBarProps) {
+  const [value, setValue] = useState(route);
+  const [focused, setFocused] = useState(false);
+
+  const commit = useCallback(() => {
+    let normalized = value.trim();
+    if (!normalized.startsWith("/")) normalized = "/" + normalized;
+    onNavigate(normalized);
+    setValue(normalized);
+  }, [value, onNavigate]);
+
+  const handleKeyDown = useCallback(
+    (e: KeyboardEvent<HTMLInputElement>) => {
+      if (e.key === "Enter") {
+        commit();
+        (e.target as HTMLInputElement).blur();
+      }
+    },
+    [commit],
+  );
+
+  const displayValue = focused ? value : route;
+
+  return (
+    <div className="flex items-center h-10 px-2 border-b border-border bg-muted/30">
+      <div className="flex items-center flex-1 h-7 rounded-md border border-border bg-background px-2 gap-1.5">
+        <svg
+          width="14"
+          height="14"
+          viewBox="0 0 24 24"
+          fill="none"
+          stroke="currentColor"
+          strokeWidth="2"
+          strokeLinecap="round"
+          strokeLinejoin="round"
+          className="text-muted-foreground shrink-0"
+        >
+          <circle cx="12" cy="12" r="10" />
+          <path d="M2 12h20" />
+          <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
+        </svg>
+        <input
+          type="text"
+          value={displayValue}
+          onChange={(e) => setValue(e.target.value)}
+          onFocus={() => {
+            setFocused(true);
+            setValue(route);
+          }}
+          onBlur={() => {
+            setFocused(false);
+            commit();
+          }}
+          onKeyDown={handleKeyDown}
+          spellCheck={false}
+          className="flex-1 bg-transparent text-xs font-mono text-foreground outline-none placeholder:text-muted-foreground"
+          placeholder="/"
+        />
+      </div>
+    </div>
+  );
+}

+ 53 - 0
examples/next-website-builder/components/ui/resizable.tsx

@@ -0,0 +1,53 @@
+"use client";
+
+import { GripVerticalIcon } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
+
+import { cn } from "@/lib/utils";
+
+function ResizablePanelGroup({
+  className,
+  ...props
+}: ResizablePrimitive.GroupProps) {
+  return (
+    <ResizablePrimitive.Group
+      data-slot="resizable-panel-group"
+      className={cn(
+        "flex h-full w-full aria-[orientation=vertical]:flex-col",
+        className,
+      )}
+      {...props}
+    />
+  );
+}
+
+function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
+  return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
+}
+
+function ResizableHandle({
+  withHandle,
+  className,
+  ...props
+}: ResizablePrimitive.SeparatorProps & {
+  withHandle?: boolean;
+}) {
+  return (
+    <ResizablePrimitive.Separator
+      data-slot="resizable-handle"
+      className={cn(
+        "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",
+        className,
+      )}
+      {...props}
+    >
+      {withHandle && (
+        <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
+          <GripVerticalIcon className="size-2.5" />
+        </div>
+      )}
+    </ResizablePrimitive.Separator>
+  );
+}
+
+export { ResizableHandle, ResizablePanel, ResizablePanelGroup };

+ 7 - 0
examples/next-website-builder/lib/app.ts

@@ -0,0 +1,7 @@
+import { createNextApp } from "@json-render/next/server";
+import { getSpec } from "./spec-store";
+
+export const { getPageData, generateMetadata, generateStaticParams } =
+  createNextApp({
+    spec: () => getSpec(),
+  });

+ 12 - 0
examples/next-website-builder/lib/catalog.ts

@@ -0,0 +1,12 @@
+import { defineCatalog } from "@json-render/core";
+import { schema } from "@json-render/next/server";
+import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
+import { websiteComponentDefinitions } from "./website-catalog";
+
+export const catalog = defineCatalog(schema, {
+  components: {
+    ...shadcnComponentDefinitions,
+    ...websiteComponentDefinitions,
+  },
+  actions: {},
+});

+ 307 - 0
examples/next-website-builder/lib/default-spec.ts

@@ -0,0 +1,307 @@
+import type { NextAppSpec } from "@json-render/next";
+
+export const defaultSpec: NextAppSpec = {
+  metadata: {
+    title: {
+      default: "Acme Inc",
+      template: "%s | Acme Inc",
+    },
+    description: "We build the future of software.",
+    icons: "/icon.svg",
+  },
+
+  state: {},
+
+  layouts: {
+    main: {
+      root: "wrapper",
+      elements: {
+        wrapper: {
+          type: "Stack",
+          props: {
+            direction: "vertical",
+            gap: "none",
+            align: "stretch",
+            className: "min-h-screen",
+          },
+          children: ["header", "content", "footer"],
+        },
+        header: {
+          type: "Header",
+          props: {
+            brand: "Acme Inc",
+            links: [
+              { label: "Home", href: "/" },
+              { label: "About", href: "/about" },
+              { label: "Contact", href: "/contact" },
+            ],
+            variant: "simple",
+          },
+        },
+        content: {
+          type: "Slot",
+          props: {},
+        },
+        footer: {
+          type: "Footer",
+          props: {
+            brand: "Acme Inc",
+            links: [
+              { label: "Home", href: "/" },
+              { label: "About", href: "/about" },
+              { label: "Contact", href: "/contact" },
+            ],
+            copyright: "2026 Acme Inc. All rights reserved.",
+            variant: "simple",
+          },
+        },
+      },
+    },
+  },
+
+  routes: {
+    "/": {
+      layout: "main",
+      metadata: {
+        title: "Home",
+        description: "Welcome to Acme Inc - we build the future of software.",
+      },
+      page: {
+        root: "page",
+        elements: {
+          page: {
+            type: "Stack",
+            props: {
+              direction: "vertical",
+              gap: "none",
+              align: "stretch",
+            },
+            children: ["hero", "features", "testimonials", "cta"],
+          },
+          hero: {
+            type: "Hero",
+            props: {
+              headline: "Build the future with Acme",
+              description:
+                "We help companies ship better software, faster. Our platform provides the tools, infrastructure, and insights you need to build world-class products.",
+              primaryCta: { label: "Get Started", href: "/contact" },
+              secondaryCta: { label: "Learn More", href: "/about" },
+              badge: "Now in Beta",
+              variant: "centered",
+            },
+          },
+          features: {
+            type: "Features",
+            props: {
+              headline: "Why Acme?",
+              description:
+                "Everything you need to build, deploy, and scale your applications.",
+              items: [
+                {
+                  title: "Lightning Fast",
+                  description:
+                    "Optimized for speed with edge-first architecture and smart caching.",
+                },
+                {
+                  title: "Secure by Default",
+                  description:
+                    "Enterprise-grade security with end-to-end encryption and SOC 2 compliance.",
+                },
+                {
+                  title: "Developer First",
+                  description:
+                    "APIs, SDKs, and CLI tools designed to fit your workflow perfectly.",
+                },
+              ],
+              columns: 3,
+              variant: "cards",
+            },
+          },
+          testimonials: {
+            type: "Testimonials",
+            props: {
+              headline: "Trusted by industry leaders",
+              items: [
+                {
+                  quote:
+                    "Acme has completely transformed how we ship products. Our deployment time went from hours to minutes.",
+                  author: "Jordan Lee",
+                  role: "VP Engineering at Globex",
+                },
+                {
+                  quote:
+                    "The developer experience is unmatched. Our team onboarded in a single afternoon.",
+                  author: "Priya Patel",
+                  role: "CTO at Initech",
+                },
+                {
+                  quote:
+                    "We cut our infrastructure costs by 40% within the first quarter of switching to Acme.",
+                  author: "Sam Ortiz",
+                  role: "Head of Platform at Hooli",
+                },
+              ],
+            },
+          },
+          cta: {
+            type: "CTA",
+            props: {
+              headline: "Ready to get started?",
+              description:
+                "Join thousands of teams building better software with Acme.",
+              buttonLabel: "Contact Us",
+              buttonHref: "/contact",
+              variant: "banner",
+            },
+          },
+        },
+      },
+    },
+
+    "/about": {
+      layout: "main",
+      metadata: {
+        title: "About",
+        description:
+          "Learn about our mission, values, and the team behind Acme.",
+      },
+      page: {
+        root: "page",
+        elements: {
+          page: {
+            type: "Stack",
+            props: {
+              direction: "vertical",
+              gap: "none",
+              align: "stretch",
+            },
+            children: ["hero", "features", "team", "cta"],
+          },
+          hero: {
+            type: "Hero",
+            props: {
+              headline: "About Acme Inc",
+              description:
+                "Founded in 2024, we are on a mission to make software development accessible to everyone. We believe the best tools should be simple, powerful, and delightful to use.",
+              variant: "centered",
+              primaryCta: null,
+              secondaryCta: null,
+              badge: null,
+            },
+          },
+          features: {
+            type: "Features",
+            props: {
+              headline: "Our Values",
+              items: [
+                {
+                  title: "Simplicity",
+                  description:
+                    "We remove complexity so you can focus on building great products.",
+                },
+                {
+                  title: "Transparency",
+                  description:
+                    "Open communication, honest pricing, and clear documentation.",
+                },
+                {
+                  title: "Craftsmanship",
+                  description:
+                    "Every detail matters. We sweat the small stuff so you don't have to.",
+                },
+              ],
+              columns: 3,
+              variant: "simple",
+              description: null,
+            },
+          },
+          team: {
+            type: "Team",
+            props: {
+              headline: "Meet the Team",
+              description:
+                "The people behind Acme who are passionate about building great developer tools.",
+              members: [
+                {
+                  name: "Alex Chen",
+                  role: "CEO & Co-founder",
+                  bio: "Previously at Vercel and Google. Passionate about developer experience.",
+                },
+                {
+                  name: "Sarah Kim",
+                  role: "CTO & Co-founder",
+                  bio: "Built distributed systems at AWS. Loves solving hard infrastructure problems.",
+                },
+                {
+                  name: "Marcus Rivera",
+                  role: "Head of Design",
+                  bio: "Former design lead at Figma and Stripe. Obsessed with clarity and craft.",
+                },
+              ],
+              variant: "grid",
+            },
+          },
+          cta: {
+            type: "CTA",
+            props: {
+              headline: "Want to join us?",
+              description:
+                "We are always looking for talented people to join our team.",
+              buttonLabel: "Get in Touch",
+              buttonHref: "/contact",
+              variant: "centered",
+            },
+          },
+        },
+      },
+    },
+
+    "/contact": {
+      layout: "main",
+      metadata: {
+        title: "Contact",
+        description: "Get in touch with the Acme team.",
+      },
+      page: {
+        root: "page",
+        elements: {
+          page: {
+            type: "Stack",
+            props: {
+              direction: "vertical",
+              gap: "none",
+              align: "stretch",
+            },
+            children: ["form"],
+          },
+          form: {
+            type: "ContactForm",
+            props: {
+              headline: "Get in Touch",
+              description:
+                "Have a question or want to work with us? Fill out the form below and we'll get back to you within 24 hours.",
+              fields: [
+                {
+                  label: "Name",
+                  type: "text" as const,
+                  placeholder: "Your name",
+                },
+                {
+                  label: "Email",
+                  type: "email" as const,
+                  placeholder: "you@example.com",
+                },
+                {
+                  label: "Message",
+                  type: "textarea" as const,
+                  placeholder: "Tell us about your project...",
+                },
+              ],
+              submitLabel: "Send Message",
+            },
+          },
+        },
+      },
+    },
+  },
+};

+ 14 - 0
examples/next-website-builder/lib/registry.tsx

@@ -0,0 +1,14 @@
+"use client";
+
+import { defineRegistry } from "@json-render/react";
+import { shadcnComponents } from "@json-render/shadcn";
+import { catalog } from "./catalog";
+import { websiteComponents } from "./website-components";
+
+export const { registry } = defineRegistry(catalog, {
+  components: {
+    ...shadcnComponents,
+    ...websiteComponents,
+  },
+  actions: {},
+});

+ 12 - 0
examples/next-website-builder/lib/spec-store.ts

@@ -0,0 +1,12 @@
+import type { NextAppSpec } from "@json-render/next";
+import { defaultSpec } from "./default-spec";
+
+let currentSpec: NextAppSpec = defaultSpec;
+
+export function getSpec(): NextAppSpec {
+  return currentSpec;
+}
+
+export function setSpec(spec: NextAppSpec): void {
+  currentSpec = spec;
+}

+ 6 - 0
examples/next-website-builder/lib/utils.ts

@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}

+ 188 - 0
examples/next-website-builder/lib/website-catalog.ts

@@ -0,0 +1,188 @@
+import { z } from "zod";
+
+const linkSchema = z.object({
+  label: z.string(),
+  href: z.string(),
+});
+
+const ctaSchema = z.object({
+  label: z.string(),
+  href: z.string(),
+});
+
+export const websiteComponentDefinitions = {
+  Header: {
+    props: z.object({
+      brand: z.string(),
+      links: z.array(linkSchema),
+      variant: z.enum(["simple", "centered"]).nullable(),
+    }),
+    description:
+      "Site header / navigation bar. Simple: brand left, links right. Centered: brand centered above links.",
+    example: {
+      brand: "Acme Inc",
+      links: [
+        { label: "Home", href: "/" },
+        { label: "About", href: "/about" },
+        { label: "Contact", href: "/contact" },
+      ],
+      variant: "simple",
+    },
+  },
+
+  Hero: {
+    props: z.object({
+      headline: z.string(),
+      description: z.string(),
+      primaryCta: ctaSchema.nullable(),
+      secondaryCta: ctaSchema.nullable(),
+      badge: z.string().nullable(),
+      variant: z.enum(["centered", "left-aligned"]).nullable(),
+    }),
+    description:
+      "Landing hero section with headline, description, and call-to-action buttons.",
+    example: {
+      headline: "Build something great",
+      description: "A modern platform for modern teams.",
+      primaryCta: { label: "Get Started", href: "/contact" },
+      variant: "centered",
+    },
+  },
+
+  Features: {
+    props: z.object({
+      headline: z.string().nullable(),
+      description: z.string().nullable(),
+      items: z.array(
+        z.object({
+          title: z.string(),
+          description: z.string(),
+        }),
+      ),
+      columns: z.number().nullable(),
+      variant: z.enum(["cards", "simple"]).nullable(),
+    }),
+    description:
+      "Feature highlights section. Cards: bordered cards per item. Simple: clean text layout.",
+    example: {
+      headline: "Why choose us",
+      items: [
+        { title: "Fast", description: "Blazing performance." },
+        { title: "Secure", description: "Enterprise-grade security." },
+        { title: "Simple", description: "Easy to use." },
+      ],
+      columns: 3,
+      variant: "cards",
+    },
+  },
+
+  Team: {
+    props: z.object({
+      headline: z.string().nullable(),
+      description: z.string().nullable(),
+      members: z.array(
+        z.object({
+          name: z.string(),
+          role: z.string(),
+          bio: z.string().nullable(),
+        }),
+      ),
+      variant: z.enum(["grid", "list"]).nullable(),
+    }),
+    description: "Team members section. Grid: card grid. List: vertical list.",
+    example: {
+      headline: "Our Team",
+      members: [
+        { name: "Alice", role: "CEO", bio: null },
+        { name: "Bob", role: "CTO", bio: null },
+      ],
+      variant: "grid",
+    },
+  },
+
+  Testimonials: {
+    props: z.object({
+      headline: z.string().nullable(),
+      items: z.array(
+        z.object({
+          quote: z.string(),
+          author: z.string(),
+          role: z.string().nullable(),
+        }),
+      ),
+    }),
+    description: "Customer testimonials / quotes section.",
+    example: {
+      headline: "What people say",
+      items: [
+        {
+          quote: "Amazing product!",
+          author: "Jane",
+          role: "CEO at Startup",
+        },
+      ],
+    },
+  },
+
+  ContactForm: {
+    props: z.object({
+      headline: z.string().nullable(),
+      description: z.string().nullable(),
+      fields: z.array(
+        z.object({
+          label: z.string(),
+          type: z.enum(["text", "email", "textarea"]),
+          placeholder: z.string().nullable(),
+        }),
+      ),
+      submitLabel: z.string(),
+    }),
+    description: "Contact form section with configurable fields.",
+    example: {
+      headline: "Get in Touch",
+      fields: [
+        { label: "Name", type: "text", placeholder: "Your name" },
+        { label: "Email", type: "email", placeholder: "you@example.com" },
+        { label: "Message", type: "textarea", placeholder: "Your message" },
+      ],
+      submitLabel: "Send",
+    },
+  },
+
+  Footer: {
+    props: z.object({
+      brand: z.string().nullable(),
+      links: z.array(linkSchema).nullable(),
+      copyright: z.string().nullable(),
+      variant: z.enum(["simple", "columns"]).nullable(),
+    }),
+    description:
+      "Site footer. Simple: centered single row. Columns: multi-column layout.",
+    example: {
+      brand: "Acme Inc",
+      copyright: "2026 Acme Inc.",
+      variant: "simple",
+    },
+  },
+
+  CTA: {
+    props: z.object({
+      headline: z.string(),
+      description: z.string().nullable(),
+      buttonLabel: z.string(),
+      buttonHref: z.string(),
+      variant: z.enum(["banner", "centered"]).nullable(),
+    }),
+    description:
+      "Call-to-action section. Banner: full-width colored background. Centered: clean centered layout.",
+    example: {
+      headline: "Ready to get started?",
+      buttonLabel: "Contact Us",
+      buttonHref: "/contact",
+      variant: "centered",
+    },
+  },
+};
+
+export type WebsiteProps<K extends keyof typeof websiteComponentDefinitions> =
+  z.output<(typeof websiteComponentDefinitions)[K]["props"]>;

+ 501 - 0
examples/next-website-builder/lib/website-components.tsx

@@ -0,0 +1,501 @@
+"use client";
+
+import type { BaseComponentProps } from "@json-render/react";
+import type { WebsiteProps } from "./website-catalog";
+
+// =============================================================================
+// Header
+// =============================================================================
+
+function HeaderComponent({
+  props,
+}: BaseComponentProps<WebsiteProps<"Header">>) {
+  const variant = props.variant ?? "simple";
+  const links = props.links ?? [];
+
+  if (variant === "centered") {
+    return (
+      <header className="border-b border-border bg-background">
+        <div className="mx-auto max-w-6xl px-6 py-6 flex flex-col items-center gap-4">
+          <span className="text-xl font-bold tracking-tight">
+            {props.brand}
+          </span>
+          <nav className="flex items-center gap-6">
+            {links.map((link) => (
+              <a
+                key={link.href}
+                href={link.href}
+                className="text-sm text-muted-foreground hover:text-foreground transition-colors"
+              >
+                {link.label}
+              </a>
+            ))}
+          </nav>
+        </div>
+      </header>
+    );
+  }
+
+  return (
+    <header className="border-b border-border bg-background sticky top-0 z-50">
+      <div className="mx-auto max-w-6xl px-6 h-16 flex items-center justify-between">
+        <span className="text-lg font-bold tracking-tight">{props.brand}</span>
+        <nav className="flex items-center gap-6">
+          {links.map((link) => (
+            <a
+              key={link.href}
+              href={link.href}
+              className="text-sm text-muted-foreground hover:text-foreground transition-colors"
+            >
+              {link.label}
+            </a>
+          ))}
+        </nav>
+      </div>
+    </header>
+  );
+}
+
+// =============================================================================
+// Hero
+// =============================================================================
+
+function HeroComponent({ props }: BaseComponentProps<WebsiteProps<"Hero">>) {
+  const variant = props.variant ?? "centered";
+  const isLeft = variant === "left-aligned";
+
+  return (
+    <section className="bg-background">
+      <div
+        className={`mx-auto max-w-4xl px-6 py-24 md:py-32 ${isLeft ? "text-left" : "text-center"}`}
+      >
+        <div
+          className={`flex flex-col gap-6 ${isLeft ? "items-start" : "items-center"}`}
+        >
+          {props.badge && (
+            <span className="inline-flex items-center rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground">
+              {props.badge}
+            </span>
+          )}
+          <h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl text-foreground">
+            {props.headline}
+          </h1>
+          <p
+            className={`text-lg text-muted-foreground ${isLeft ? "max-w-xl" : "max-w-2xl"}`}
+          >
+            {props.description}
+          </p>
+          {(props.primaryCta || props.secondaryCta) && (
+            <div className="flex flex-wrap gap-3 pt-2">
+              {props.primaryCta && (
+                <a
+                  href={props.primaryCta.href}
+                  className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
+                >
+                  {props.primaryCta.label}
+                </a>
+              )}
+              {props.secondaryCta && (
+                <a
+                  href={props.secondaryCta.href}
+                  className="inline-flex h-11 items-center justify-center rounded-md border border-border bg-background px-6 text-sm font-medium text-foreground hover:bg-muted transition-colors"
+                >
+                  {props.secondaryCta.label}
+                </a>
+              )}
+            </div>
+          )}
+        </div>
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// Features
+// =============================================================================
+
+function FeaturesComponent({
+  props,
+}: BaseComponentProps<WebsiteProps<"Features">>) {
+  const variant = props.variant ?? "cards";
+  const columns = props.columns ?? 3;
+  const items = props.items ?? [];
+
+  const colClass =
+    columns === 2
+      ? "sm:grid-cols-2"
+      : columns === 4
+        ? "sm:grid-cols-2 lg:grid-cols-4"
+        : "sm:grid-cols-2 lg:grid-cols-3";
+
+  return (
+    <section className="bg-background">
+      <div className="mx-auto max-w-6xl px-6 py-20">
+        {(props.headline || props.description) && (
+          <div className="mb-12 text-center">
+            {props.headline && (
+              <h2 className="text-3xl font-bold tracking-tight text-foreground">
+                {props.headline}
+              </h2>
+            )}
+            {props.description && (
+              <p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
+                {props.description}
+              </p>
+            )}
+          </div>
+        )}
+        <div className={`grid grid-cols-1 gap-6 ${colClass}`}>
+          {items.map((item, i) =>
+            variant === "cards" ? (
+              <div
+                key={i}
+                className="rounded-lg border border-border bg-card p-6 shadow-sm"
+              >
+                <h3 className="text-lg font-semibold text-card-foreground">
+                  {item.title}
+                </h3>
+                <p className="mt-2 text-sm text-muted-foreground">
+                  {item.description}
+                </p>
+              </div>
+            ) : (
+              <div key={i} className="py-2">
+                <h3 className="text-lg font-semibold text-foreground">
+                  {item.title}
+                </h3>
+                <p className="mt-1 text-sm text-muted-foreground">
+                  {item.description}
+                </p>
+              </div>
+            ),
+          )}
+        </div>
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// Team
+// =============================================================================
+
+function TeamComponent({ props }: BaseComponentProps<WebsiteProps<"Team">>) {
+  const variant = props.variant ?? "grid";
+  const members = props.members ?? [];
+
+  return (
+    <section className="bg-background">
+      <div className="mx-auto max-w-6xl px-6 py-20">
+        {(props.headline || props.description) && (
+          <div className="mb-12 text-center">
+            {props.headline && (
+              <h2 className="text-3xl font-bold tracking-tight text-foreground">
+                {props.headline}
+              </h2>
+            )}
+            {props.description && (
+              <p className="mt-3 text-lg text-muted-foreground max-w-2xl mx-auto">
+                {props.description}
+              </p>
+            )}
+          </div>
+        )}
+        {variant === "list" ? (
+          <div className="space-y-6 max-w-2xl mx-auto">
+            {members.map((m, i) => (
+              <div key={i} className="flex flex-col gap-1">
+                <h3 className="text-lg font-semibold text-foreground">
+                  {m.name}
+                </h3>
+                <p className="text-sm text-muted-foreground">{m.role}</p>
+                {m.bio && (
+                  <p className="mt-1 text-sm text-muted-foreground">{m.bio}</p>
+                )}
+              </div>
+            ))}
+          </div>
+        ) : (
+          <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
+            {members.map((m, i) => (
+              <div
+                key={i}
+                className="rounded-lg border border-border bg-card p-6 text-center"
+              >
+                <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted text-lg font-bold text-muted-foreground">
+                  {m.name
+                    .split(" ")
+                    .map((n) => n[0])
+                    .join("")
+                    .slice(0, 2)
+                    .toUpperCase()}
+                </div>
+                <h3 className="text-base font-semibold text-card-foreground">
+                  {m.name}
+                </h3>
+                <p className="text-sm text-muted-foreground">{m.role}</p>
+                {m.bio && (
+                  <p className="mt-2 text-sm text-muted-foreground">{m.bio}</p>
+                )}
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// Testimonials
+// =============================================================================
+
+function TestimonialsComponent({
+  props,
+}: BaseComponentProps<WebsiteProps<"Testimonials">>) {
+  const items = props.items ?? [];
+
+  return (
+    <section className="bg-muted/30">
+      <div className="mx-auto max-w-6xl px-6 py-20">
+        {props.headline && (
+          <h2 className="mb-12 text-center text-3xl font-bold tracking-tight text-foreground">
+            {props.headline}
+          </h2>
+        )}
+        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
+          {items.map((item, i) => (
+            <blockquote
+              key={i}
+              className="rounded-lg border border-border bg-background p-6"
+            >
+              <p className="text-sm text-foreground leading-relaxed">
+                &ldquo;{item.quote}&rdquo;
+              </p>
+              <footer className="mt-4 flex flex-col">
+                <span className="text-sm font-semibold text-foreground">
+                  {item.author}
+                </span>
+                {item.role && (
+                  <span className="text-xs text-muted-foreground">
+                    {item.role}
+                  </span>
+                )}
+              </footer>
+            </blockquote>
+          ))}
+        </div>
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// ContactForm
+// =============================================================================
+
+function ContactFormComponent({
+  props,
+}: BaseComponentProps<WebsiteProps<"ContactForm">>) {
+  const fields = props.fields ?? [];
+
+  return (
+    <section className="bg-background">
+      <div className="mx-auto max-w-xl px-6 py-20">
+        {(props.headline || props.description) && (
+          <div className="mb-8 text-center">
+            {props.headline && (
+              <h2 className="text-3xl font-bold tracking-tight text-foreground">
+                {props.headline}
+              </h2>
+            )}
+            {props.description && (
+              <p className="mt-3 text-muted-foreground">{props.description}</p>
+            )}
+          </div>
+        )}
+        <form
+          className="rounded-lg border border-border bg-card p-6 shadow-sm"
+          onSubmit={(e) => e.preventDefault()}
+        >
+          <div className="flex flex-col gap-4">
+            {fields.map((field, i) =>
+              field.type === "textarea" ? (
+                <div key={i} className="flex flex-col gap-1.5">
+                  <label className="text-sm font-medium text-foreground">
+                    {field.label}
+                  </label>
+                  <textarea
+                    placeholder={field.placeholder ?? ""}
+                    rows={4}
+                    className="rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
+                  />
+                </div>
+              ) : (
+                <div key={i} className="flex flex-col gap-1.5">
+                  <label className="text-sm font-medium text-foreground">
+                    {field.label}
+                  </label>
+                  <input
+                    type={field.type}
+                    placeholder={field.placeholder ?? ""}
+                    className="h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
+                  />
+                </div>
+              ),
+            )}
+            <button
+              type="submit"
+              className="mt-2 h-11 rounded-md bg-primary px-6 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
+            >
+              {props.submitLabel}
+            </button>
+          </div>
+        </form>
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// Footer
+// =============================================================================
+
+function FooterComponent({
+  props,
+}: BaseComponentProps<WebsiteProps<"Footer">>) {
+  const variant = props.variant ?? "simple";
+  const links = props.links ?? [];
+
+  if (variant === "columns") {
+    return (
+      <footer className="border-t border-border bg-muted/30">
+        <div className="mx-auto max-w-6xl px-6 py-12">
+          <div className="flex flex-col gap-8 sm:flex-row sm:justify-between">
+            <div>
+              {props.brand && (
+                <span className="text-lg font-bold text-foreground">
+                  {props.brand}
+                </span>
+              )}
+            </div>
+            {links.length > 0 && (
+              <nav className="flex flex-col gap-2 sm:flex-row sm:gap-6">
+                {links.map((link) => (
+                  <a
+                    key={link.href}
+                    href={link.href}
+                    className="text-sm text-muted-foreground hover:text-foreground transition-colors"
+                  >
+                    {link.label}
+                  </a>
+                ))}
+              </nav>
+            )}
+          </div>
+          {props.copyright && (
+            <p className="mt-8 text-xs text-muted-foreground">
+              {props.copyright}
+            </p>
+          )}
+        </div>
+      </footer>
+    );
+  }
+
+  return (
+    <footer className="border-t border-border bg-muted/30">
+      <div className="mx-auto max-w-6xl px-6 py-10 flex flex-col items-center gap-4">
+        {props.brand && (
+          <span className="text-sm font-semibold text-foreground">
+            {props.brand}
+          </span>
+        )}
+        {links.length > 0 && (
+          <nav className="flex items-center gap-4">
+            {links.map((link) => (
+              <a
+                key={link.href}
+                href={link.href}
+                className="text-xs text-muted-foreground hover:text-foreground transition-colors"
+              >
+                {link.label}
+              </a>
+            ))}
+          </nav>
+        )}
+        {props.copyright && (
+          <p className="text-xs text-muted-foreground">{props.copyright}</p>
+        )}
+      </div>
+    </footer>
+  );
+}
+
+// =============================================================================
+// CTA
+// =============================================================================
+
+function CTAComponent({ props }: BaseComponentProps<WebsiteProps<"CTA">>) {
+  const variant = props.variant ?? "centered";
+
+  if (variant === "banner") {
+    return (
+      <section className="bg-primary text-primary-foreground">
+        <div className="mx-auto max-w-6xl px-6 py-16 flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
+          <div>
+            <h2 className="text-2xl font-bold">{props.headline}</h2>
+            {props.description && (
+              <p className="mt-1 text-sm opacity-90">{props.description}</p>
+            )}
+          </div>
+          <a
+            href={props.buttonHref}
+            className="inline-flex h-11 items-center justify-center rounded-md bg-background px-6 text-sm font-medium text-foreground shadow-sm hover:bg-background/90 transition-colors shrink-0"
+          >
+            {props.buttonLabel}
+          </a>
+        </div>
+      </section>
+    );
+  }
+
+  return (
+    <section className="bg-muted/30">
+      <div className="mx-auto max-w-3xl px-6 py-20 text-center">
+        <h2 className="text-3xl font-bold tracking-tight text-foreground">
+          {props.headline}
+        </h2>
+        {props.description && (
+          <p className="mt-3 text-lg text-muted-foreground">
+            {props.description}
+          </p>
+        )}
+        <div className="mt-8">
+          <a
+            href={props.buttonHref}
+            className="inline-flex h-11 items-center justify-center rounded-md bg-primary px-8 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
+          >
+            {props.buttonLabel}
+          </a>
+        </div>
+      </div>
+    </section>
+  );
+}
+
+// =============================================================================
+// Export map
+// =============================================================================
+
+export const websiteComponents = {
+  Header: HeaderComponent,
+  Hero: HeroComponent,
+  Features: FeaturesComponent,
+  Team: TeamComponent,
+  Testimonials: TestimonialsComponent,
+  ContactForm: ContactFormComponent,
+  Footer: FooterComponent,
+  CTA: CTAComponent,
+};

+ 6 - 0
examples/next-website-builder/next-env.d.ts

@@ -0,0 +1,6 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+import "./.next/dev/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 12 - 0
examples/next-website-builder/next.config.ts

@@ -0,0 +1,12 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+  allowedDevOrigins: ["next-website-builder-demo.json-render.localhost"],
+  transpilePackages: [
+    "@json-render/core",
+    "@json-render/react",
+    "@json-render/shadcn",
+  ],
+};
+
+export default nextConfig;

+ 41 - 0
examples/next-website-builder/package.json

@@ -0,0 +1,41 @@
+{
+  "name": "example-next-website-builder",
+  "version": "0.1.0",
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "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)",
+    "dev": "portless next-website-builder-demo.json-render next dev --turbopack",
+    "build": "next build",
+    "start": "next start",
+    "check-types": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@json-render/core": "workspace:*",
+    "@json-render/react": "workspace:*",
+    "@json-render/next": "workspace:*",
+    "@json-render/shadcn": "workspace:*",
+    "@visual-json/react": "0.3.1",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "geist": "^1.7.0",
+    "lucide-react": "^1.6.0",
+    "next": "16.2.1",
+    "radix-ui": "^1.4.3",
+    "react": "19.2.4",
+    "react-dom": "19.2.4",
+    "react-resizable-panels": "^4.7.6",
+    "tailwind-merge": "^3.5.0",
+    "zod": "4.3.6"
+  },
+  "devDependencies": {
+    "@internal/typescript-config": "workspace:*",
+    "@tailwindcss/postcss": "^4.2.2",
+    "@types/node": "^22.10.0",
+    "@types/react": "19.2.3",
+    "@types/react-dom": "19.2.3",
+    "tailwindcss": "^4.2.2",
+    "tw-animate-css": "^1.4.0",
+    "typescript": "^5.7.2"
+  }
+}

+ 5 - 0
examples/next-website-builder/postcss.config.mjs

@@ -0,0 +1,5 @@
+export default {
+  plugins: {
+    "@tailwindcss/postcss": {},
+  },
+};

+ 13 - 0
examples/next-website-builder/tsconfig.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../../packages/typescript-config/nextjs.json",
+  "compilerOptions": {
+    "plugins": [{ "name": "next" }],
+    "declaration": false,
+    "declarationMap": false,
+    "paths": {
+      "@/*": ["./*"]
+    }
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}

+ 103 - 0
packages/next/README.md

@@ -0,0 +1,103 @@
+# @json-render/next
+
+Next.js renderer for [@json-render/core](https://json-render.dev). JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
+
+## Installation
+
+```bash
+npm install @json-render/core @json-render/react @json-render/next
+```
+
+## Quick Start
+
+### 1. Define your application spec
+
+```typescript
+import type { NextAppSpec } from "@json-render/next";
+
+const spec: NextAppSpec = {
+  metadata: {
+    title: { default: "My App", template: "%s | My App" },
+  },
+  layouts: {
+    main: {
+      root: "shell",
+      elements: {
+        shell: { type: "Container", props: {}, children: ["nav", "slot"] },
+        nav: { type: "NavBar", props: {}, children: [] },
+        slot: { type: "Slot", props: {}, children: [] },
+      },
+    },
+  },
+  routes: {
+    "/": {
+      layout: "main",
+      metadata: { title: "Home" },
+      page: { root: "hero", elements: { hero: { type: "Card", props: { title: "Welcome" }, children: [] } } },
+    },
+    "/about": {
+      layout: "main",
+      metadata: { title: "About" },
+      page: { root: "content", elements: { content: { type: "Card", props: { title: "About" }, children: [] } } },
+    },
+  },
+};
+```
+
+### 2. Create the app
+
+```typescript
+// lib/app.ts
+import { createNextApp } from "@json-render/next/server";
+
+export const { Page, generateMetadata, generateStaticParams } = createNextApp({ spec });
+```
+
+### 3. Wire up Next.js routes
+
+```tsx
+// app/[[...slug]]/page.tsx
+export { Page as default, generateMetadata, generateStaticParams } from "@/lib/app";
+```
+
+```tsx
+// app/[[...slug]]/layout.tsx
+import { NextAppProvider } from "@json-render/next";
+import { registry, handlers } from "@/lib/registry";
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+  return (
+    <html lang="en">
+      <body>
+        <NextAppProvider registry={registry} handlers={handlers}>
+          {children}
+        </NextAppProvider>
+      </body>
+    </html>
+  );
+}
+```
+
+## Features
+
+- **Pages as spec** -- Define entire multi-page apps in JSON
+- **Route matching** -- Dynamic segments (`[slug]`), catch-all (`[...path]`), optional catch-all (`[[...path]]`)
+- **Nested layouts** -- Reusable layouts with `Slot` component for content injection
+- **SEO metadata** -- Per-route metadata with title templates, OpenGraph, Twitter cards
+- **SSR** -- Server-side rendering via Next.js App Router
+- **Data loaders** -- Server-side async data loading before page render
+- **Static generation** -- `generateStaticParams` for pre-rendering
+- **Client navigation** -- Built-in `Link` component wrapping `next/link`
+- **Error/Loading/NotFound** -- Per-route error boundaries, loading states, and 404 pages
+- **AI streaming** -- Generate entire apps with JSONL patches via `SpecStream`
+
+## Entry Points
+
+| Import                     | Description                                      |
+| -------------------------- | ------------------------------------------------ |
+| `@json-render/next`        | Client components (NextAppProvider, PageRenderer) |
+| `@json-render/next/server` | Server utilities (createNextApp, matchRoute)      |
+
+## Documentation
+
+See the [json-render documentation](https://json-render.dev/docs/api/next) for full API reference.

+ 71 - 0
packages/next/package.json

@@ -0,0 +1,71 @@
+{
+  "name": "@json-render/next",
+  "version": "0.15.0",
+  "license": "Apache-2.0",
+  "description": "Next.js renderer for @json-render/core. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.",
+  "keywords": [
+    "json",
+    "ui",
+    "next",
+    "nextjs",
+    "ai",
+    "generative-ui",
+    "llm",
+    "renderer",
+    "streaming",
+    "ssr",
+    "pages",
+    "routing"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vercel-labs/json-render.git",
+    "directory": "packages/next"
+  },
+  "homepage": "https://json-render.dev",
+  "bugs": {
+    "url": "https://github.com/vercel-labs/json-render/issues"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "main": "./dist/index.js",
+  "module": "./dist/index.mjs",
+  "types": "./dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.mjs",
+      "require": "./dist/index.js"
+    },
+    "./server": {
+      "types": "./dist/server.d.ts",
+      "import": "./dist/server.mjs",
+      "require": "./dist/server.js"
+    }
+  },
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "build": "tsup",
+    "dev": "tsup --watch",
+    "check-types": "tsc --noEmit",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@json-render/core": "workspace:*",
+    "@json-render/react": "workspace:*"
+  },
+  "devDependencies": {
+    "@internal/typescript-config": "workspace:*",
+    "@types/react": "19.2.14",
+    "next": "16.2.1",
+    "tsup": "^8.0.2",
+    "typescript": "^5.4.5"
+  },
+  "peerDependencies": {
+    "next": "^16.2.0",
+    "react": "^19.2.3"
+  }
+}

+ 17 - 0
packages/next/src/catalog-types.ts

@@ -0,0 +1,17 @@
+/**
+ * Catalog-aware types for @json-render/next.
+ *
+ * Re-exports the React catalog types since Next.js uses the same
+ * component and action model as @json-render/react.
+ */
+export type {
+  EventHandle,
+  BaseComponentProps,
+  SetState,
+  StateModel,
+  ComponentContext,
+  ComponentFn,
+  Components,
+  ActionFn,
+  Actions,
+} from "@json-render/react";

+ 69 - 0
packages/next/src/components/error-boundary.tsx

@@ -0,0 +1,69 @@
+"use client";
+
+import React from "react";
+import type { Spec } from "@json-render/core";
+import { Renderer, type ComponentRegistry } from "@json-render/react";
+import { useNextApp } from "./provider";
+
+/**
+ * Props for NextErrorBoundary.
+ */
+export interface NextErrorBoundaryProps {
+  /** The error that occurred */
+  error: Error & { digest?: string };
+  /** Function to attempt re-rendering the page */
+  reset: () => void;
+  /** Optional error UI spec from the matched route */
+  errorSpec?: Spec | null;
+}
+
+/**
+ * Error boundary component for json-render Next.js apps.
+ *
+ * If the matched route defines an `error` spec, it renders that.
+ * Otherwise renders a minimal default error UI.
+ *
+ * Must be used in a file with `"use client"` directive (Next.js requirement).
+ */
+export function NextErrorBoundary({
+  error,
+  reset,
+  errorSpec,
+}: NextErrorBoundaryProps) {
+  let registry: ComponentRegistry | undefined;
+  try {
+    const ctx = useNextApp();
+    registry = ctx.registry;
+  } catch {
+    // Provider may not be available in error boundaries
+  }
+
+  if (errorSpec && registry) {
+    const augmentedRegistry: ComponentRegistry = {
+      ...registry,
+      Slot: () => null,
+    };
+    return <Renderer spec={errorSpec} registry={augmentedRegistry} />;
+  }
+
+  return (
+    <div style={{ padding: "2rem", textAlign: "center" }}>
+      <h2 style={{ marginBottom: "1rem" }}>Something went wrong</h2>
+      <p style={{ color: "#666", marginBottom: "1.5rem" }}>
+        {error.message || "An unexpected error occurred."}
+      </p>
+      <button
+        onClick={reset}
+        style={{
+          padding: "0.5rem 1rem",
+          borderRadius: "0.375rem",
+          border: "1px solid #ccc",
+          background: "#fff",
+          cursor: "pointer",
+        }}
+      >
+        Try again
+      </button>
+    </div>
+  );
+}

+ 52 - 0
packages/next/src/components/link.tsx

@@ -0,0 +1,52 @@
+"use client";
+
+import React, { type ReactNode } from "react";
+import NextLink from "next/link";
+import type { ComponentRenderProps } from "@json-render/react";
+
+/**
+ * Props expected by the Link component in json-render specs.
+ */
+export interface LinkProps {
+  /** The target URL or route path */
+  href: string;
+  /** Whether to replace the current history entry */
+  replace?: boolean;
+  /** Whether to prefetch the linked route */
+  prefetch?: boolean;
+  /** CSS class name */
+  className?: string;
+  /** Inline styles */
+  style?: React.CSSProperties;
+}
+
+/**
+ * Built-in Link component for json-render Next.js specs.
+ *
+ * Wraps `next/link` for client-side navigation between routes.
+ *
+ * Usage in a spec:
+ * ```json
+ * {
+ *   "type": "Link",
+ *   "props": { "href": "/about" },
+ *   "children": ["link-text"]
+ * }
+ * ```
+ */
+export function Link({ element, children }: ComponentRenderProps<LinkProps>) {
+  const { href, replace, prefetch, className, style } =
+    element.props as LinkProps;
+
+  return (
+    <NextLink
+      href={href}
+      replace={replace}
+      prefetch={prefetch}
+      className={className}
+      style={style}
+    >
+      {children}
+    </NextLink>
+  );
+}

+ 45 - 0
packages/next/src/components/loading-renderer.tsx

@@ -0,0 +1,45 @@
+import React from "react";
+import type { Spec } from "@json-render/core";
+
+/**
+ * Props for NextLoading.
+ */
+export interface NextLoadingProps {
+  /** Optional loading UI spec */
+  loadingSpec?: Spec | null;
+}
+
+/**
+ * Loading component for json-render Next.js apps.
+ *
+ * If a loading spec is provided, it will be rendered by the PageRenderer.
+ * Otherwise renders a minimal default loading indicator.
+ *
+ * Note: Since Next.js loading.tsx is static (no params), this renders
+ * a generic loading state. Route-specific loading can be achieved with
+ * Suspense boundaries in page content.
+ */
+export function NextLoading({ loadingSpec: _loadingSpec }: NextLoadingProps) {
+  return (
+    <div
+      style={{
+        display: "flex",
+        justifyContent: "center",
+        alignItems: "center",
+        minHeight: "200px",
+      }}
+    >
+      <div
+        style={{
+          width: "2rem",
+          height: "2rem",
+          border: "2px solid #e5e7eb",
+          borderTopColor: "#3b82f6",
+          borderRadius: "50%",
+          animation: "jr-spin 0.6s linear infinite",
+        }}
+      />
+      <style>{`@keyframes jr-spin { to { transform: rotate(360deg) } }`}</style>
+    </div>
+  );
+}

+ 27 - 0
packages/next/src/components/not-found-renderer.tsx

@@ -0,0 +1,27 @@
+import React from "react";
+
+/**
+ * Not-found component for json-render Next.js apps.
+ *
+ * Renders when the pathname does not match any route in the spec.
+ */
+export function NextNotFound() {
+  return (
+    <div
+      style={{
+        display: "flex",
+        flexDirection: "column",
+        alignItems: "center",
+        justifyContent: "center",
+        minHeight: "400px",
+        padding: "2rem",
+        textAlign: "center",
+      }}
+    >
+      <h1 style={{ fontSize: "4rem", fontWeight: 700, margin: 0 }}>404</h1>
+      <p style={{ color: "#666", marginTop: "0.5rem", fontSize: "1.125rem" }}>
+        This page could not be found.
+      </p>
+    </div>
+  );
+}

+ 135 - 0
packages/next/src/components/page-renderer.tsx

@@ -0,0 +1,135 @@
+"use client";
+
+import React, { useMemo, type ReactNode } from "react";
+import type { Spec } from "@json-render/core";
+import {
+  Renderer,
+  StateProvider,
+  VisibilityProvider,
+  ValidationProvider,
+  ActionProvider,
+  type ComponentRegistry,
+  type ComponentRenderProps,
+} from "@json-render/react";
+import { useNextApp } from "./provider";
+
+/**
+ * Props for PageRenderer.
+ */
+export interface PageRendererProps {
+  /** Page element tree spec */
+  spec: Spec;
+  /** Initial state (merged from spec.state, global state, and loader data) */
+  initialState?: Record<string, unknown>;
+  /** Optional layout element tree spec */
+  layoutSpec?: Spec | null;
+  /** Whether the spec is currently loading/streaming */
+  loading?: boolean;
+}
+
+/**
+ * Built-in Slot component that renders children passed into layouts.
+ */
+function SlotComponent({ children }: ComponentRenderProps) {
+  return <>{children}</>;
+}
+
+/**
+ * Client component that renders a json-render page within the Next.js app.
+ *
+ * Reads the component registry and action handlers from NextAppProvider context.
+ * Handles layout rendering by injecting page content into a Slot component.
+ */
+export function PageRenderer({
+  spec,
+  initialState,
+  layoutSpec,
+  loading,
+}: PageRendererProps) {
+  const { registry, handlers, navigate } = useNextApp();
+
+  const augmentedRegistry: ComponentRegistry = useMemo(() => {
+    return {
+      ...registry,
+      Slot: SlotComponent,
+    };
+  }, [registry]);
+
+  const actionHandlers = useMemo(() => {
+    const base: Record<
+      string,
+      (params: Record<string, unknown>) => Promise<unknown> | unknown
+    > = { ...handlers };
+
+    base.navigate = (params: Record<string, unknown>) => {
+      const href = params.href as string | undefined;
+      if (href) navigate(href);
+    };
+
+    return base;
+  }, [handlers, navigate]);
+
+  const pageContent = (
+    <Renderer spec={spec} registry={augmentedRegistry} loading={loading} />
+  );
+
+  if (layoutSpec) {
+    return (
+      <StateProvider initialState={initialState}>
+        <VisibilityProvider>
+          <ValidationProvider>
+            <ActionProvider handlers={actionHandlers} navigate={navigate}>
+              <LayoutWithSlot
+                layoutSpec={layoutSpec}
+                registry={augmentedRegistry}
+                loading={loading}
+              >
+                {pageContent}
+              </LayoutWithSlot>
+            </ActionProvider>
+          </ValidationProvider>
+        </VisibilityProvider>
+      </StateProvider>
+    );
+  }
+
+  return (
+    <StateProvider initialState={initialState}>
+      <VisibilityProvider>
+        <ValidationProvider>
+          <ActionProvider handlers={actionHandlers} navigate={navigate}>
+            {pageContent}
+          </ActionProvider>
+        </ValidationProvider>
+      </VisibilityProvider>
+    </StateProvider>
+  );
+}
+
+/**
+ * Renders a layout spec and injects children into the Slot.
+ */
+function LayoutWithSlot({
+  layoutSpec,
+  registry,
+  loading,
+  children,
+}: {
+  layoutSpec: Spec;
+  registry: ComponentRegistry;
+  loading?: boolean;
+  children: ReactNode;
+}) {
+  const layoutRegistry: ComponentRegistry = useMemo(() => {
+    return {
+      ...registry,
+      Slot: function LayoutSlot() {
+        return <>{children}</>;
+      },
+    };
+  }, [registry, children]);
+
+  return (
+    <Renderer spec={layoutSpec} registry={layoutRegistry} loading={loading} />
+  );
+}

+ 92 - 0
packages/next/src/components/provider.tsx

@@ -0,0 +1,92 @@
+"use client";
+
+import React, { createContext, useContext, type ReactNode } from "react";
+import { useRouter } from "next/navigation";
+import type { ComponentRegistry } from "@json-render/react";
+
+/**
+ * Context value holding the component registry, action handlers,
+ * and the Next.js router navigate function.
+ */
+export interface NextAppContextValue {
+  registry: ComponentRegistry;
+  handlers?: Record<
+    string,
+    (params: Record<string, unknown>) => Promise<unknown> | unknown
+  >;
+  navigate: (href: string) => void;
+}
+
+const NextAppContext = createContext<NextAppContextValue | null>(null);
+
+/**
+ * Props for NextAppProvider.
+ */
+export interface NextAppProviderProps {
+  /** Component registry for rendering */
+  registry: ComponentRegistry;
+  /** Action handlers */
+  handlers?: Record<
+    string,
+    (params: Record<string, unknown>) => Promise<unknown> | unknown
+  >;
+  children: ReactNode;
+}
+
+/**
+ * Provider that holds the component registry and action handlers
+ * for the entire json-render Next.js application.
+ *
+ * Wrap your root layout with this provider:
+ *
+ * ```tsx
+ * // app/[[...slug]]/layout.tsx
+ * import { NextAppProvider } from "@json-render/next";
+ * import { registry, handlers } from "@/lib/registry";
+ *
+ * export default function Layout({ children }) {
+ *   return (
+ *     <html><body>
+ *       <NextAppProvider registry={registry} handlers={handlers}>
+ *         {children}
+ *       </NextAppProvider>
+ *     </body></html>
+ *   );
+ * }
+ * ```
+ */
+export function NextAppProvider({
+  registry,
+  handlers,
+  children,
+}: NextAppProviderProps) {
+  const router = useRouter();
+  const navigate = React.useCallback(
+    (href: string) => router.push(href),
+    [router],
+  );
+
+  const value = React.useMemo(
+    () => ({ registry, handlers, navigate }),
+    [registry, handlers, navigate],
+  );
+
+  return (
+    <NextAppContext.Provider value={value}>{children}</NextAppContext.Provider>
+  );
+}
+
+/**
+ * Hook to access the NextApp context.
+ * Must be used within a NextAppProvider.
+ */
+export function useNextApp(): NextAppContextValue {
+  const ctx = useContext(NextAppContext);
+  if (!ctx) {
+    throw new Error(
+      "[json-render/next] useNextApp must be used within a <NextAppProvider>. " +
+        "Wrap your root layout with <NextAppProvider registry={...} handlers={...}>.",
+    );
+  }
+  return ctx;
+}

+ 148 - 0
packages/next/src/create-app.ts

@@ -0,0 +1,148 @@
+import { notFound } from "next/navigation";
+import type {
+  NextAppSpec,
+  CreateNextAppOptions,
+  NextAppExports,
+  PageData,
+} from "./types";
+import { matchRoute, slugToPath, collectStaticParams } from "./router";
+import { resolveMetadata, type ResolvedMetadata } from "./metadata";
+
+/**
+ * Resolve the spec from the options — supports both static specs
+ * and factory functions that return specs (sync or async).
+ */
+async function resolveSpec(
+  specOrFn: NextAppSpec | (() => NextAppSpec | Promise<NextAppSpec>),
+): Promise<NextAppSpec> {
+  if (typeof specOrFn === "function") {
+    return await specOrFn();
+  }
+  return specOrFn;
+}
+
+/**
+ * Merge state from multiple sources: global state, page state, and loader data.
+ * Later sources override earlier ones.
+ */
+function mergeState(
+  ...sources: (Record<string, unknown> | undefined | null)[]
+): Record<string, unknown> {
+  const result: Record<string, unknown> = {};
+  for (const source of sources) {
+    if (source) {
+      Object.assign(result, source);
+    }
+  }
+  return result;
+}
+
+/**
+ * Create a fully wired Next.js application from a json-render spec.
+ *
+ * Returns `getPageData`, `generateMetadata`, and `generateStaticParams`
+ * ready to be used in Next.js `[[...slug]]` catch-all routes.
+ *
+ * @example
+ * ```typescript
+ * // lib/app.ts
+ * import { createNextApp } from "@json-render/next/server";
+ *
+ * export const { getPageData, generateMetadata, generateStaticParams } =
+ *   createNextApp({ spec });
+ *
+ * // app/[[...slug]]/page.tsx (Server Component)
+ * import { notFound } from "next/navigation";
+ * import { getPageData, generateMetadata, generateStaticParams } from "@/lib/app";
+ * import { SiteRenderer } from "./renderer";
+ *
+ * export { generateMetadata, generateStaticParams };
+ *
+ * export default async function Page({ params }) {
+ *   const data = await getPageData({ params });
+ *   if (!data) notFound();
+ *   return <SiteRenderer {...data} />;
+ * }
+ *
+ * // app/[[...slug]]/renderer.tsx ("use client")
+ * import { PageRenderer } from "@json-render/next";
+ * export function SiteRenderer(props) {
+ *   return <PageRenderer {...props} />;
+ * }
+ * ```
+ */
+export function createNextApp(options: CreateNextAppOptions): NextAppExports {
+  const { spec: specOrFn, loaders } = options;
+
+  /**
+   * Resolve page data for a given set of route params.
+   * Returns null when no route matches.
+   */
+  async function getPageData({
+    params,
+  }: {
+    params: Promise<{ slug?: string[] }>;
+  }): Promise<PageData | null> {
+    const { slug } = await params;
+    const pathname = slugToPath(slug);
+    const spec = await resolveSpec(specOrFn);
+
+    const matched = matchRoute(spec, pathname);
+
+    if (!matched) {
+      return null;
+    }
+
+    const { route } = matched;
+
+    let loaderData: Record<string, unknown> | undefined;
+    if (route.loader && loaders?.[route.loader]) {
+      loaderData = await loaders[route.loader]!(matched.params);
+    }
+
+    const initialState = mergeState(
+      spec.state,
+      route.page.state as Record<string, unknown> | undefined,
+      loaderData,
+    );
+
+    const layoutSpec =
+      route.layout && spec.layouts
+        ? (spec.layouts[route.layout] ?? null)
+        : null;
+
+    return {
+      spec: route.page,
+      initialState:
+        Object.keys(initialState).length > 0 ? initialState : undefined,
+      layoutSpec,
+    };
+  }
+
+  /**
+   * Generate metadata for the matched route.
+   * Merges global spec metadata with route-specific metadata.
+   */
+  async function generateMetadata({
+    params,
+  }: {
+    params: Promise<{ slug?: string[] }>;
+  }): Promise<ResolvedMetadata> {
+    const { slug } = await params;
+    const pathname = slugToPath(slug);
+    const spec = await resolveSpec(specOrFn);
+    const matched = matchRoute(spec, pathname);
+    return resolveMetadata(spec, matched?.route ?? null);
+  }
+
+  /**
+   * Generate static params for all statically-known routes.
+   * Used by Next.js to pre-render pages at build time.
+   */
+  async function generateStaticParams(): Promise<{ slug: string[] }[]> {
+    const spec = await resolveSpec(specOrFn);
+    return collectStaticParams(spec);
+  }
+
+  return { getPageData, generateMetadata, generateStaticParams };
+}

+ 73 - 0
packages/next/src/index.ts

@@ -0,0 +1,73 @@
+"use client";
+
+// =============================================================================
+// @json-render/next — Client exports
+//
+// This entry point includes client components and is safe to import
+// in "use client" modules and Next.js pages/layouts.
+// =============================================================================
+
+// Provider
+export {
+  NextAppProvider,
+  useNextApp,
+  type NextAppProviderProps,
+  type NextAppContextValue,
+} from "./components/provider";
+
+// Page renderer
+export {
+  PageRenderer,
+  type PageRendererProps,
+} from "./components/page-renderer";
+
+// Error boundary
+export {
+  NextErrorBoundary,
+  type NextErrorBoundaryProps,
+} from "./components/error-boundary";
+
+// Loading
+export {
+  NextLoading,
+  type NextLoadingProps,
+} from "./components/loading-renderer";
+
+// Not found
+export { NextNotFound } from "./components/not-found-renderer";
+
+// Link component
+export { Link, type LinkProps } from "./components/link";
+
+// Types (re-exported for convenience)
+export type {
+  NextAppSpec,
+  NextRouteSpec,
+  NextMetadata,
+  MatchedRoute,
+  LoaderFn,
+  CreateNextAppOptions,
+  NextAppExports,
+  PageData,
+} from "./types";
+
+// Catalog types (re-exported from @json-render/react)
+export type {
+  EventHandle,
+  BaseComponentProps,
+  SetState,
+  StateModel,
+  ComponentContext,
+  ComponentFn,
+  Components,
+  ActionFn,
+  Actions,
+} from "./catalog-types";
+
+// Core types (re-exported for convenience)
+export type { Spec, StateStore } from "@json-render/core";
+export { createStateStore } from "@json-render/core";
+export type {
+  ComponentRegistry,
+  ComponentRenderProps,
+} from "@json-render/react";

+ 101 - 0
packages/next/src/metadata.ts

@@ -0,0 +1,101 @@
+import type { NextMetadata, NextAppSpec, NextRouteSpec } from "./types";
+
+/**
+ * Metadata result compatible with Next.js's Metadata type.
+ * Uses plain objects to avoid importing Next.js types at runtime.
+ */
+export type ResolvedMetadata = Record<string, unknown>;
+
+/**
+ * Resolve metadata for a route by merging global spec metadata
+ * with route-specific metadata.
+ *
+ * Follows Next.js metadata merging semantics:
+ * - Route metadata overrides global metadata for scalar fields
+ * - Title templates from global metadata apply to route titles
+ * - OpenGraph and Twitter fields are shallow-merged
+ */
+export function resolveMetadata(
+  spec: NextAppSpec,
+  route?: NextRouteSpec | null,
+): ResolvedMetadata {
+  const globalMeta = spec.metadata;
+  const routeMeta = route?.metadata;
+
+  if (!globalMeta && !routeMeta) return {};
+
+  const result: ResolvedMetadata = {};
+
+  const mergedTitle = resolveTitle(globalMeta?.title, routeMeta?.title);
+  if (mergedTitle !== undefined) result.title = mergedTitle;
+
+  const description = routeMeta?.description ?? globalMeta?.description;
+  if (description) result.description = description;
+
+  const keywords = routeMeta?.keywords ?? globalMeta?.keywords;
+  if (keywords) result.keywords = keywords;
+
+  const openGraph = mergeObject(globalMeta?.openGraph, routeMeta?.openGraph);
+  if (openGraph) result.openGraph = openGraph;
+
+  const twitter = mergeObject(globalMeta?.twitter, routeMeta?.twitter);
+  if (twitter) result.twitter = twitter;
+
+  const robots = routeMeta?.robots ?? globalMeta?.robots;
+  if (robots) result.robots = robots;
+
+  const alternates = routeMeta?.alternates ?? globalMeta?.alternates;
+  if (alternates) result.alternates = alternates;
+
+  const icons = routeMeta?.icons ?? globalMeta?.icons;
+  if (icons) result.icons = icons;
+
+  return result;
+}
+
+/**
+ * Resolve a title value considering templates.
+ *
+ * If global metadata defines a title template (e.g. "%s | My App"),
+ * route titles are interpolated into it.
+ */
+function resolveTitle(
+  globalTitle: NextMetadata["title"],
+  routeTitle: NextMetadata["title"],
+): unknown {
+  if (!routeTitle && !globalTitle) return undefined;
+
+  if (!routeTitle) {
+    if (typeof globalTitle === "string") return globalTitle;
+    if (typeof globalTitle === "object" && globalTitle !== null) {
+      return globalTitle.default ?? undefined;
+    }
+    return undefined;
+  }
+
+  if (typeof routeTitle === "object" && routeTitle !== null) {
+    if (routeTitle.absolute) return routeTitle.absolute;
+    if (routeTitle.template || routeTitle.default) return routeTitle;
+    return routeTitle;
+  }
+
+  if (
+    typeof globalTitle === "object" &&
+    globalTitle !== null &&
+    globalTitle.template
+  ) {
+    return globalTitle.template.replace("%s", routeTitle as string);
+  }
+
+  return routeTitle;
+}
+
+function mergeObject(
+  base: Record<string, unknown> | undefined,
+  override: Record<string, unknown> | undefined,
+): Record<string, unknown> | undefined {
+  if (!base && !override) return undefined;
+  if (!base) return override;
+  if (!override) return base;
+  return { ...base, ...override };
+}

+ 4 - 0
packages/next/src/page-renderer-client.tsx

@@ -0,0 +1,4 @@
+"use client";
+
+export { PageRenderer } from "./components/page-renderer";
+export type { PageRendererProps } from "./components/page-renderer";

+ 196 - 0
packages/next/src/router.ts

@@ -0,0 +1,196 @@
+import type { NextAppSpec, MatchedRoute } from "./types";
+
+interface CompiledRoute {
+  pattern: string;
+  regex: RegExp;
+  paramNames: string[];
+  /** Whether the last segment is catch-all or optional catch-all */
+  catchAll: boolean;
+  optionalCatchAll: boolean;
+  /** Number of static segments (higher = more specific) */
+  specificity: number;
+}
+
+/**
+ * Compile a Next.js route pattern into a regex matcher.
+ *
+ * Supports:
+ * - Static segments: `/about`, `/blog`
+ * - Dynamic segments: `/blog/[slug]`
+ * - Catch-all segments: `/docs/[...path]`
+ * - Optional catch-all segments: `/settings/[[...path]]`
+ */
+function compileRoute(pattern: string): CompiledRoute {
+  const paramNames: string[] = [];
+  let catchAll = false;
+  let optionalCatchAll = false;
+  let specificity = 0;
+
+  const segments = pattern === "/" ? [""] : pattern.split("/").slice(1);
+  const regexParts: string[] = [];
+
+  for (const segment of segments) {
+    if (segment.startsWith("[[...") && segment.endsWith("]]")) {
+      const paramName = segment.slice(5, -2);
+      paramNames.push(paramName);
+      optionalCatchAll = true;
+      regexParts.push("(?:/(.+))?");
+    } else if (segment.startsWith("[...") && segment.endsWith("]")) {
+      const paramName = segment.slice(4, -1);
+      paramNames.push(paramName);
+      catchAll = true;
+      regexParts.push("/(.+)");
+    } else if (segment.startsWith("[") && segment.endsWith("]")) {
+      const paramName = segment.slice(1, -1);
+      paramNames.push(paramName);
+      regexParts.push("/([^/]+)");
+    } else {
+      specificity++;
+      regexParts.push(`/${escapeRegExp(segment)}`);
+    }
+  }
+
+  const regexStr = pattern === "/" ? "^/$" : `^${regexParts.join("")}$`;
+
+  return {
+    pattern,
+    regex: new RegExp(regexStr),
+    paramNames,
+    catchAll,
+    optionalCatchAll,
+    specificity,
+  };
+}
+
+function escapeRegExp(str: string): string {
+  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/**
+ * Match a pathname against a spec's routes.
+ *
+ * Routes are matched in order of specificity:
+ * 1. Exact/static matches first (most specific)
+ * 2. Dynamic segment matches
+ * 3. Catch-all matches (least specific)
+ * 4. Optional catch-all matches (fallback)
+ *
+ * @returns The matched route with extracted params, or null if no match.
+ */
+export function matchRoute(
+  spec: NextAppSpec,
+  pathname: string,
+): MatchedRoute | null {
+  const normalizedPath = pathname === "" ? "/" : pathname;
+
+  const compiled = Object.keys(spec.routes).map(compileRoute);
+
+  compiled.sort((a, b) => {
+    if (a.optionalCatchAll !== b.optionalCatchAll) {
+      return a.optionalCatchAll ? 1 : -1;
+    }
+    if (a.catchAll !== b.catchAll) {
+      return a.catchAll ? 1 : -1;
+    }
+    if (a.specificity !== b.specificity) {
+      return b.specificity - a.specificity;
+    }
+    return a.paramNames.length - b.paramNames.length;
+  });
+
+  for (const route of compiled) {
+    const match = route.regex.exec(normalizedPath);
+    if (!match) continue;
+
+    const params: Record<string, string | string[]> = {};
+    for (let i = 0; i < route.paramNames.length; i++) {
+      const value = match[i + 1];
+      const name = route.paramNames[i]!;
+
+      if (route.catchAll || route.optionalCatchAll) {
+        params[name] = value ? value.split("/") : [];
+      } else {
+        params[name] = value ?? "";
+      }
+    }
+
+    return {
+      route: spec.routes[route.pattern]!,
+      pattern: route.pattern,
+      params,
+    };
+  }
+
+  return null;
+}
+
+/**
+ * Convert a Next.js catch-all slug array to a pathname.
+ *
+ * @example
+ * slugToPath(undefined)     // "/"
+ * slugToPath([])            // "/"
+ * slugToPath(["blog"])      // "/blog"
+ * slugToPath(["blog","hi"]) // "/blog/hi"
+ */
+export function slugToPath(slug: string[] | undefined): string {
+  if (!slug || slug.length === 0) return "/";
+  return "/" + slug.join("/");
+}
+
+/**
+ * Collect all static params from the spec for generateStaticParams.
+ * Returns params suitable for Next.js [[...slug]] catch-all routes.
+ */
+export function collectStaticParams(spec: NextAppSpec): { slug: string[] }[] {
+  const results: { slug: string[] }[] = [];
+
+  for (const [pattern, route] of Object.entries(spec.routes)) {
+    if (route.staticParams) {
+      for (const paramSet of route.staticParams) {
+        const slug = buildSlugFromPattern(pattern, paramSet);
+        if (slug) results.push({ slug });
+      }
+    } else if (!pattern.includes("[")) {
+      const slug = pattern === "/" ? [] : pattern.slice(1).split("/");
+      results.push({ slug });
+    }
+  }
+
+  return results;
+}
+
+/**
+ * Build a slug array from a route pattern and a set of params.
+ */
+function buildSlugFromPattern(
+  pattern: string,
+  params: Record<string, string>,
+): string[] | null {
+  if (pattern === "/") return [];
+
+  const segments = pattern.split("/").slice(1);
+  const result: string[] = [];
+
+  for (const segment of segments) {
+    if (segment.startsWith("[[...") && segment.endsWith("]]")) {
+      const paramName = segment.slice(5, -2);
+      const value = params[paramName];
+      if (value) result.push(...value.split("/"));
+    } else if (segment.startsWith("[...") && segment.endsWith("]")) {
+      const paramName = segment.slice(4, -1);
+      const value = params[paramName];
+      if (!value) return null;
+      result.push(...value.split("/"));
+    } else if (segment.startsWith("[") && segment.endsWith("]")) {
+      const paramName = segment.slice(1, -1);
+      const value = params[paramName];
+      if (!value) return null;
+      result.push(value);
+    } else {
+      result.push(segment);
+    }
+  }
+
+  return result;
+}

+ 332 - 0
packages/next/src/schema.ts

@@ -0,0 +1,332 @@
+import { defineSchema, type PromptContext } from "@json-render/core";
+
+/**
+ * Prompt template for Next.js app generation.
+ *
+ * Teaches the AI to generate multi-page applications with routes,
+ * layouts, metadata, and page content using JSONL patches.
+ */
+function nextAppPromptTemplate(context: PromptContext): string {
+  const { catalog, options, formatZodType } = context;
+  const {
+    system = "You are a Next.js application generator.",
+    customRules = [],
+  } = options;
+
+  const lines: string[] = [];
+  lines.push(system);
+  lines.push("");
+
+  lines.push("OUTPUT FORMAT:");
+  lines.push(
+    "Output JSONL (one JSON object per line) with RFC 6902 JSON Patch operations to build a Next.js application spec.",
+  );
+  lines.push(
+    "The spec defines routes (pages), layouts, metadata, and state for a full Next.js app.",
+  );
+  lines.push("");
+  lines.push("Example output (each line is a separate JSON object):");
+  lines.push("");
+  lines.push(
+    `{"op":"add","path":"/metadata","value":{"title":{"default":"My App","template":"%s | My App"},"description":"A full Next.js application"}}`,
+  );
+  lines.push(`{"op":"add","path":"/layouts","value":{}}`);
+  lines.push(
+    `{"op":"add","path":"/layouts/main","value":{"root":"shell","elements":{"shell":{"type":"AppShell","props":{},"children":["nav","slot"]},"nav":{"type":"NavBar","props":{"links":[{"href":"/","label":"Home"},{"href":"/about","label":"About"}]},"children":[]},"slot":{"type":"Slot","props":{},"children":[]}}}}`,
+  );
+  lines.push(`{"op":"add","path":"/routes","value":{}}`);
+  lines.push(
+    `{"op":"add","path":"/routes/~1","value":{"layout":"main","metadata":{"title":"Home"},"page":{"root":"hero","elements":{"hero":{"type":"Card","props":{"title":"Welcome"},"children":[]}}}}}`,
+  );
+  lines.push(
+    `{"op":"add","path":"/routes/~1about","value":{"layout":"main","metadata":{"title":"About"},"page":{"root":"content","elements":{"content":{"type":"Card","props":{"title":"About Us"},"children":[]}}}}}`,
+  );
+  lines.push("");
+
+  lines.push("SPEC STRUCTURE:");
+  lines.push("The top-level spec has these fields:");
+  lines.push(
+    "  - metadata: Root-level SEO metadata (title template, description, openGraph, twitter)",
+  );
+  lines.push(
+    "  - layouts: Reusable layout element trees. Each layout MUST include a { type: 'Slot' } element where page content will be injected.",
+  );
+  lines.push("  - routes: Route definitions keyed by URL pattern");
+  lines.push("  - state: Global initial state shared across all routes");
+  lines.push("");
+
+  lines.push("ROUTES:");
+  lines.push("Route keys use Next.js URL patterns:");
+  lines.push("  - '/' - home page");
+  lines.push("  - '/about' - static route");
+  lines.push("  - '/blog/[slug]' - dynamic segment");
+  lines.push("  - '/docs/[...path]' - catch-all segment");
+  lines.push("  - '/settings/[[...path]]' - optional catch-all segment");
+  lines.push("");
+  lines.push(
+    "IMPORTANT: In JSON Patch paths, forward slashes in route keys must be escaped as ~1.",
+  );
+  lines.push("  - Route '/' becomes path '/routes/~1'");
+  lines.push("  - Route '/about' becomes path '/routes/~1about'");
+  lines.push("  - Route '/blog/[slug]' becomes path '/routes/~1blog~1[slug]'");
+  lines.push("");
+
+  lines.push("Each route has:");
+  lines.push(
+    "  - page: Element tree (root + elements + optional state) — the page content",
+  );
+  lines.push(
+    "  - metadata: Per-route SEO metadata (title, description, openGraph)",
+  );
+  lines.push("  - layout: Layout key referencing an entry in the layouts map");
+  lines.push("  - loading: Optional loading state element tree");
+  lines.push("  - error: Optional error state element tree");
+  lines.push("  - loader: Optional server-side data loader name");
+  lines.push("");
+
+  lines.push("LAYOUTS:");
+  lines.push(
+    "Layouts wrap page content. A layout element tree MUST include a component with type 'Slot' — this is where the page content will be rendered.",
+  );
+  lines.push(
+    "Layouts are defined once and reused across routes via the layout field.",
+  );
+  lines.push("");
+
+  lines.push("METADATA:");
+  lines.push("Root metadata sets defaults. Route metadata overrides per page.");
+  lines.push(
+    "  - title: string or { default, template } (use %s for page title in template)",
+  );
+  lines.push("  - description: string");
+  lines.push("  - openGraph: { title, description, images, type }");
+  lines.push("  - twitter: { card, title, description }");
+  lines.push("");
+
+  lines.push("PAGE CONTENT:");
+  lines.push(
+    "Each page uses the standard json-render element tree format: root, elements, optional state.",
+  );
+  lines.push(
+    "Elements have type, props, children, and optionally visible, on, repeat, watch fields.",
+  );
+  lines.push("");
+
+  const catalogData = catalog as {
+    components?: Record<
+      string,
+      {
+        description?: string;
+        props?: unknown;
+        slots?: string[];
+        events?: string[];
+      }
+    >;
+    actions?: Record<string, { description?: string }>;
+  };
+
+  if (catalogData.components) {
+    lines.push(
+      `AVAILABLE COMPONENTS (${Object.keys(catalogData.components).length}):`,
+    );
+    lines.push("");
+    for (const [name, def] of Object.entries(catalogData.components)) {
+      const propsStr = def.props ? formatZodType(def.props as any) : "{}";
+      const hasChildren = def.slots && def.slots.length > 0;
+      const childrenStr = hasChildren ? " [accepts children]" : "";
+      const eventsStr =
+        def.events && def.events.length > 0
+          ? ` [events: ${def.events.join(", ")}]`
+          : "";
+      const descStr = def.description ? ` - ${def.description}` : "";
+      lines.push(`- ${name}: ${propsStr}${descStr}${childrenStr}${eventsStr}`);
+    }
+    lines.push("");
+    lines.push("Built-in components (always available):");
+    lines.push(
+      "- Slot: {} - Placeholder in layouts where page content is rendered. Required in every layout.",
+    );
+    lines.push(
+      "- Link: { href: string } [accepts children] - Client-side navigation link (renders as next/link).",
+    );
+    lines.push("");
+  }
+
+  if (catalogData.actions && Object.keys(catalogData.actions).length > 0) {
+    lines.push("AVAILABLE ACTIONS:");
+    lines.push("");
+    for (const [name, def] of Object.entries(catalogData.actions)) {
+      lines.push(`- ${name}${def.description ? `: ${def.description}` : ""}`);
+    }
+    lines.push("");
+  }
+
+  lines.push("BUILT-IN ACTIONS:");
+  lines.push(
+    "- setState: Update a value in the state model. Params: { statePath: string, value: any }",
+  );
+  lines.push(
+    "- pushState: Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }",
+  );
+  lines.push(
+    "- removeState: Remove an item from an array by index. Params: { statePath: string, index: number }",
+  );
+  lines.push("- navigate: Navigate to a route. Params: { href: string }");
+  lines.push("");
+
+  lines.push("RULES:");
+  const baseRules = [
+    "Output ONLY JSONL patches - one JSON object per line, no markdown, no code fences",
+    "First add /metadata with the app-level metadata including title template",
+    "Then add /layouts with reusable layout definitions (each must have a Slot component)",
+    "Then add /routes with each route's page, metadata, and layout reference",
+    "Every layout MUST include a { type: 'Slot' } element where page content is injected",
+    "ONLY use components listed in AVAILABLE COMPONENTS (plus built-in Slot and Link)",
+    "Each element needs: type, props, children (array of child keys)",
+    "Use unique keys for element map entries",
+    "Escape forward slashes in route keys as ~1 in JSON Patch paths",
+    "Include realistic sample data in state for data-driven pages",
+    "Use Link component for navigation between routes",
+    "Create a cohesive multi-page app with consistent layouts and navigation",
+  ];
+  const allRules = [...baseRules, ...customRules];
+  allRules.forEach((rule, i) => {
+    lines.push(`${i + 1}. ${rule}`);
+  });
+
+  return lines.join("\n");
+}
+
+/**
+ * The schema for @json-render/next.
+ *
+ * Defines a multi-page Next.js application structure:
+ * - Spec: Routes with pages, layouts, metadata, loading/error states
+ * - Catalog: Components with props schemas, and optional actions
+ *
+ * This schema is fundamentally different from the React element tree schema.
+ * It's page-based, designed for full Next.js applications with SSR,
+ * routing, and metadata generation.
+ */
+export const schema = defineSchema(
+  (s) => ({
+    spec: s.object({
+      /** Root-level metadata applied as defaults */
+      metadata: s.object({
+        title: s.any(),
+        description: s.string(),
+        keywords: s.array(s.string()),
+        openGraph: s.any(),
+        twitter: s.any(),
+        robots: s.any(),
+        icons: s.any(),
+      }),
+
+      /** Route definitions keyed by URL pattern */
+      routes: s.record(
+        s.object({
+          /** Page element tree */
+          page: s.object({
+            root: s.string(),
+            elements: s.record(
+              s.object({
+                type: s.ref("catalog.components"),
+                props: s.propsOf("catalog.components"),
+                children: s.array(s.string()),
+                visible: s.any(),
+              }),
+            ),
+            state: s.any(),
+          }),
+          /** Per-route metadata */
+          metadata: s.any(),
+          /** Layout key */
+          layout: s.string(),
+          /** Loading state element tree */
+          loading: s.any(),
+          /** Error state element tree */
+          error: s.any(),
+          /** Not-found element tree */
+          notFound: s.any(),
+          /** Server-side data loader name */
+          loader: s.string(),
+          /** Static params for SSG */
+          staticParams: s.any(),
+        }),
+      ),
+
+      /** Reusable layout element trees */
+      layouts: s.record(
+        s.object({
+          root: s.string(),
+          elements: s.record(
+            s.object({
+              type: s.ref("catalog.components"),
+              props: s.propsOf("catalog.components"),
+              children: s.array(s.string()),
+              visible: s.any(),
+            }),
+          ),
+          state: s.any(),
+        }),
+      ),
+
+      /** Global initial state */
+      state: s.any(),
+    }),
+
+    catalog: s.object({
+      /** Component definitions */
+      components: s.map({
+        props: s.zod(),
+        slots: s.array(s.string()),
+        description: s.string(),
+        example: s.any(),
+      }),
+      /** Action definitions (optional) */
+      actions: s.map({
+        params: s.zod(),
+        description: s.string(),
+      }),
+    }),
+  }),
+  {
+    promptTemplate: nextAppPromptTemplate,
+    builtInActions: [
+      {
+        name: "setState",
+        description:
+          "Update a value in the state model at the given statePath. Params: { statePath: string, value: any }",
+      },
+      {
+        name: "pushState",
+        description:
+          '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.',
+      },
+      {
+        name: "removeState",
+        description:
+          "Remove an item from an array in state by index. Params: { statePath: string, index: number }",
+      },
+      {
+        name: "navigate",
+        description:
+          "Navigate to a route within the app. Params: { href: string }",
+      },
+    ],
+  },
+);
+
+/**
+ * Type for the Next.js schema
+ */
+export type NextSchema = typeof schema;
+
+/**
+ * Infer the spec type from a catalog
+ */
+export type NextSpec<TCatalog> = typeof schema extends {
+  createCatalog: (catalog: TCatalog) => { _specType: infer S };
+}
+  ? S
+  : never;

+ 55 - 0
packages/next/src/server.ts

@@ -0,0 +1,55 @@
+/**
+ * @json-render/next/server — Server-safe exports
+ *
+ * This entry point does not import any client components or React hooks.
+ * Safe to use in:
+ * - Server Components
+ * - API routes
+ * - Build scripts
+ * - Server Actions
+ *
+ * @example
+ * ```ts
+ * import { createNextApp, schema } from "@json-render/next/server";
+ * ```
+ */
+
+// createNextApp (main API)
+export { createNextApp } from "./create-app";
+
+// Schema (Next.js app spec format)
+export { schema, type NextSchema, type NextSpec } from "./schema";
+
+// Router utilities
+export { matchRoute, slugToPath, collectStaticParams } from "./router";
+
+// Metadata resolution
+export { resolveMetadata, type ResolvedMetadata } from "./metadata";
+
+// Types
+export type {
+  NextAppSpec,
+  NextRouteSpec,
+  NextMetadata,
+  MatchedRoute,
+  LoaderFn,
+  CreateNextAppOptions,
+  NextAppExports,
+  PageData,
+} from "./types";
+
+// Catalog types (type-only, no runtime)
+export type {
+  EventHandle,
+  BaseComponentProps,
+  SetState,
+  StateModel,
+  ComponentContext,
+  ComponentFn,
+  Components,
+  ActionFn,
+  Actions,
+} from "./catalog-types";
+
+// Core types (re-exported for convenience)
+export type { Spec, StateStore } from "@json-render/core";

+ 176 - 0
packages/next/src/types.ts

@@ -0,0 +1,176 @@
+import type { Spec } from "@json-render/core";
+
+/**
+ * Metadata for Next.js pages and layouts.
+ * Maps to Next.js's Metadata type.
+ */
+export interface NextMetadata {
+  /** Page title — string or template config */
+  title?:
+    | string
+    | {
+        /** Default title when no child overrides */
+        default: string;
+        /** Template string, use %s for the page title (e.g. "%s | My App") */
+        template?: string;
+        /** Absolute title that ignores parent templates */
+        absolute?: string;
+      };
+  /** Meta description */
+  description?: string;
+  /** Keywords for SEO */
+  keywords?: string[];
+  /** Open Graph metadata */
+  openGraph?: {
+    title?: string;
+    description?: string;
+    images?: string | string[];
+    type?: string;
+    url?: string;
+    siteName?: string;
+    locale?: string;
+  };
+  /** Twitter card metadata */
+  twitter?: {
+    card?: "summary" | "summary_large_image" | "app" | "player";
+    title?: string;
+    description?: string;
+    images?: string | string[];
+    creator?: string;
+    site?: string;
+  };
+  /** Robots directives */
+  robots?: string | { index?: boolean; follow?: boolean };
+  /** Canonical URL */
+  alternates?: {
+    canonical?: string;
+  };
+  /** Favicon and icons */
+  icons?: string | { icon?: string; apple?: string; shortcut?: string };
+}
+
+/**
+ * A route definition within a NextAppSpec.
+ */
+export interface NextRouteSpec {
+  /** Page content — standard json-render element tree */
+  page: Spec;
+  /** Per-route metadata for SEO */
+  metadata?: NextMetadata;
+  /** Layout key referencing an entry in NextAppSpec.layouts */
+  layout?: string;
+  /** Loading state element tree (rendered during Suspense) */
+  loading?: Spec;
+  /** Error state element tree */
+  error?: Spec;
+  /** Not-found element tree (for dynamic routes that don't match data) */
+  notFound?: Spec;
+  /** Server-side data loader name (references loaders in createNextApp) */
+  loader?: string;
+  /** Static params for generateStaticParams */
+  staticParams?: Record<string, string>[];
+}
+
+/**
+ * The full application spec for @json-render/next.
+ *
+ * Describes an entire Next.js application: routes, pages, layouts,
+ * metadata, and server-side data loading.
+ *
+ * Routes are keyed by URL patterns using Next.js conventions:
+ * - `"/"` — exact match
+ * - `"/blog/[slug]"` — dynamic segment
+ * - `"/docs/[...path]"` — catch-all segment
+ * - `"/settings/[[...path]]"` — optional catch-all segment
+ */
+export interface NextAppSpec {
+  /** Root-level metadata applied to all routes (layouts can override) */
+  metadata?: NextMetadata;
+  /** Route definitions keyed by URL pattern */
+  routes: Record<string, NextRouteSpec>;
+  /**
+   * Reusable layout element trees. Each layout spec must include a
+   * `Slot` component type where page content will be injected.
+   */
+  layouts?: Record<string, Spec>;
+  /** Global initial state shared across all routes */
+  state?: Record<string, unknown>;
+}
+
+/**
+ * Result of matching a pathname against the spec's routes.
+ */
+export interface MatchedRoute {
+  /** The matched route spec */
+  route: NextRouteSpec;
+  /** The URL pattern that matched (e.g. "/blog/[slug]") */
+  pattern: string;
+  /** Extracted route parameters (e.g. { slug: "hello-world" }) */
+  params: Record<string, string | string[]>;
+}
+
+/**
+ * Server-side data loader function signature.
+ * Receives route params and returns data to merge into initial state.
+ */
+export type LoaderFn = (
+  params: Record<string, string | string[]>,
+) => Promise<Record<string, unknown>> | Record<string, unknown>;
+
+/**
+ * Options for createNextApp.
+ */
+export interface CreateNextAppOptions {
+  /** The application spec */
+  spec: NextAppSpec | (() => NextAppSpec | Promise<NextAppSpec>);
+  /** Server-side data loaders keyed by name */
+  loaders?: Record<string, LoaderFn>;
+}
+
+/**
+ * Data returned by getPageData for client-side rendering.
+ */
+export interface PageData {
+  /** Page element tree spec */
+  spec: Spec;
+  /** Initial state (merged from spec.state, global state, and loader data) */
+  initialState?: Record<string, unknown>;
+  /** Optional layout element tree spec */
+  layoutSpec?: Spec | null;
+}
+
+/**
+ * The result of createNextApp — exports for Next.js route files.
+ */
+export interface NextAppExports {
+  /**
+   * Resolve page data for a given set of params.
+   * Returns null if no route matches (caller should call notFound()).
+   *
+   * Use this in your Server Component page.tsx alongside a client wrapper
+   * that renders PageRenderer:
+   *
+   * ```tsx
+   * // app/[[...slug]]/page.tsx
+   * import { getPageData, generateMetadata, generateStaticParams } from "@/lib/app";
+   * import { WebsiteRenderer } from "./renderer";
+   *
+   * export { generateMetadata, generateStaticParams };
+   *
+   * export default async function Page({ params }) {
+   *   const data = await getPageData({ params });
+   *   if (!data) notFound();
+   *   return <WebsiteRenderer {...data} />;
+   * }
+   * ```
+   */
+  getPageData: (props: {
+    params: Promise<{ slug?: string[] }>;
+  }) => Promise<PageData | null>;
+  /** generateMetadata function for page.tsx */
+  generateMetadata: (props: {
+    params: Promise<{ slug?: string[] }>;
+  }) => Promise<Record<string, unknown>>;
+  /** generateStaticParams function for page.tsx */
+  generateStaticParams: () => Promise<{ slug: string[] }[]>;
+}

+ 9 - 0
packages/next/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "@internal/typescript-config/react-library.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "include": ["src"],
+  "exclude": ["node_modules", "dist"]
+}

+ 35 - 0
packages/next/tsup.config.ts

@@ -0,0 +1,35 @@
+import { defineConfig } from "tsup";
+
+const sharedExternal = [
+  "react",
+  "react-dom",
+  "next",
+  "next/link",
+  "next/navigation",
+  "@json-render/core",
+  "@json-render/react",
+];
+
+export default defineConfig([
+  {
+    entry: {
+      index: "src/index.ts",
+      "page-renderer-client": "src/page-renderer-client.tsx",
+    },
+    format: ["cjs", "esm"],
+    dts: true,
+    sourcemap: true,
+    splitting: true,
+    clean: true,
+    banner: { js: '"use client";' },
+    external: sharedExternal,
+  },
+  {
+    entry: { server: "src/server.ts" },
+    format: ["cjs", "esm"],
+    dts: true,
+    sourcemap: true,
+    splitting: false,
+    external: [...sharedExternal, /page-renderer-client/],
+  },
+]);

+ 5 - 2
packages/shadcn/src/catalog.ts

@@ -37,6 +37,7 @@ export const shadcnComponentDefinitions = {
       description: z.string().nullable(),
       maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(),
       centered: z.boolean().nullable(),
+      className: z.string().nullable().describe("Additional CSS classes"),
     }),
     slots: ["default"],
     description:
@@ -47,11 +48,12 @@ export const shadcnComponentDefinitions = {
   Stack: {
     props: z.object({
       direction: z.enum(["horizontal", "vertical"]).nullable(),
-      gap: z.enum(["none", "sm", "md", "lg"]).nullable(),
+      gap: z.enum(["none", "sm", "md", "lg", "xl"]).nullable(),
       align: z.enum(["start", "center", "end", "stretch"]).nullable(),
       justify: z
         .enum(["start", "center", "end", "between", "around"])
         .nullable(),
+      className: z.string().nullable().describe("Additional CSS classes"),
     }),
     slots: ["default"],
     description: "Flex container for layouts",
@@ -61,7 +63,8 @@ export const shadcnComponentDefinitions = {
   Grid: {
     props: z.object({
       columns: z.number().nullable(),
-      gap: z.enum(["sm", "md", "lg"]).nullable(),
+      gap: z.enum(["sm", "md", "lg", "xl"]).nullable(),
+      className: z.string().nullable().describe("Additional CSS classes"),
     }),
     slots: ["default"],
     description: "Grid layout (1-6 columns)",

+ 16 - 3
packages/shadcn/src/components.tsx

@@ -185,7 +185,7 @@ export const shadcnComponents = {
     const centeredClass = props.centered ? "mx-auto" : "";
 
     return (
-      <Card className={cn(maxWidthClass, centeredClass)}>
+      <Card className={cn(maxWidthClass, centeredClass, props.className)}>
         {(props.title || props.description) && (
           <CardHeader>
             {props.title && <CardTitle>{props.title}</CardTitle>}
@@ -206,6 +206,7 @@ export const shadcnComponents = {
       sm: "gap-2",
       md: "gap-3",
       lg: "gap-4",
+      xl: "gap-6",
     };
     const alignMap: Record<string, string> = {
       start: "items-start",
@@ -227,7 +228,14 @@ export const shadcnComponents = {
 
     return (
       <div
-        className={`flex ${isHorizontal ? "flex-row flex-wrap" : "flex-col"} ${gapClass} ${alignClass} ${justifyClass}`}
+        className={cn(
+          "flex",
+          isHorizontal ? "flex-row flex-wrap" : "flex-col",
+          gapClass,
+          alignClass,
+          justifyClass,
+          props.className,
+        )}
       >
         {children}
       </div>
@@ -247,13 +255,18 @@ export const shadcnComponents = {
       sm: "gap-2",
       md: "gap-3",
       lg: "gap-4",
+      xl: "gap-6",
     };
 
     const n = Math.max(1, Math.min(6, props.columns ?? 1));
     const cols = colsMap[n] ?? "grid-cols-1";
     const gridGap = gridGapMap[props.gap ?? "md"] ?? "gap-3";
 
-    return <div className={`grid ${cols} ${gridGap}`}>{children}</div>;
+    return (
+      <div className={cn("grid", cols, gridGap, props.className)}>
+        {children}
+      </div>
+    );
   },
 
   Separator: ({ props }: BaseComponentProps<ShadcnProps<"Separator">>) => {

+ 295 - 18
pnpm-lock.yaml

@@ -754,6 +754,82 @@ importers:
         specifier: ^2.3.0
         version: 2.3.0(rollup@4.55.1)(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))
 
+  examples/next-website-builder:
+    dependencies:
+      '@json-render/core':
+        specifier: workspace:*
+        version: link:../../packages/core
+      '@json-render/next':
+        specifier: workspace:*
+        version: link:../../packages/next
+      '@json-render/react':
+        specifier: workspace:*
+        version: link:../../packages/react
+      '@json-render/shadcn':
+        specifier: workspace:*
+        version: link:../../packages/shadcn
+      '@visual-json/react':
+        specifier: 0.3.1
+        version: 0.3.1(react@19.2.4)
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      geist:
+        specifier: ^1.7.0
+        version: 1.7.0(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+      lucide-react:
+        specifier: ^1.6.0
+        version: 1.6.0(react@19.2.4)
+      next:
+        specifier: 16.2.1
+        version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      radix-ui:
+        specifier: ^1.4.3
+        version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      react:
+        specifier: 19.2.4
+        version: 19.2.4
+      react-dom:
+        specifier: 19.2.4
+        version: 19.2.4(react@19.2.4)
+      react-resizable-panels:
+        specifier: ^4.7.6
+        version: 4.7.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      tailwind-merge:
+        specifier: ^3.5.0
+        version: 3.5.0
+      zod:
+        specifier: 4.3.6
+        version: 4.3.6
+    devDependencies:
+      '@internal/typescript-config':
+        specifier: workspace:*
+        version: link:../../packages/typescript-config
+      '@tailwindcss/postcss':
+        specifier: ^4.2.2
+        version: 4.2.2
+      '@types/node':
+        specifier: ^22.10.0
+        version: 22.19.6
+      '@types/react':
+        specifier: 19.2.3
+        version: 19.2.3
+      '@types/react-dom':
+        specifier: 19.2.3
+        version: 19.2.3(@types/react@19.2.3)
+      tailwindcss:
+        specifier: ^4.2.2
+        version: 4.2.2
+      tw-animate-css:
+        specifier: ^1.4.0
+        version: 1.4.0
+      typescript:
+        specifier: ^5.7.2
+        version: 5.9.3
+
   examples/no-ai:
     dependencies:
       '@json-render/core':
@@ -1613,7 +1689,7 @@ importers:
         version: 19.2.3
       tsup:
         specifier: ^8.0.2
-        version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+        version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
       typescript:
         specifier: ^5.4.5
         version: 5.9.3
@@ -1671,6 +1747,34 @@ importers:
         specifier: ^5.4.5
         version: 5.9.3
 
+  packages/next:
+    dependencies:
+      '@json-render/core':
+        specifier: workspace:*
+        version: link:../core
+      '@json-render/react':
+        specifier: workspace:*
+        version: link:../react
+      react:
+        specifier: ^19.2.3
+        version: 19.2.4
+    devDependencies:
+      '@internal/typescript-config':
+        specifier: workspace:*
+        version: link:../typescript-config
+      '@types/react':
+        specifier: 19.2.14
+        version: 19.2.14
+      next:
+        specifier: 16.2.1
+        version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      tsup:
+        specifier: ^8.0.2
+        version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
+      typescript:
+        specifier: ^5.4.5
+        version: 5.9.3
+
   packages/react:
     dependencies:
       '@json-render/core':
@@ -4314,6 +4418,9 @@ packages:
   '@next/env@16.1.6':
     resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
 
+  '@next/env@16.2.1':
+    resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==}
+
   '@next/eslint-plugin-next@15.5.9':
     resolution: {integrity: sha512-kUzXx0iFiXw27cQAViE1yKWnz/nF8JzRmwgMRTMh8qMY90crNsdXJRh2e+R0vBpFR3kk1yvAR7wev7+fCCb79Q==}
 
@@ -4340,6 +4447,12 @@ packages:
     cpu: [arm64]
     os: [darwin]
 
+  '@next/swc-darwin-arm64@16.2.1':
+    resolution: {integrity: sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@next/swc-darwin-x64@16.1.1':
     resolution: {integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==}
     engines: {node: '>= 10'}
@@ -4352,6 +4465,12 @@ packages:
     cpu: [x64]
     os: [darwin]
 
+  '@next/swc-darwin-x64@16.2.1':
+    resolution: {integrity: sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
   '@next/swc-linux-arm64-gnu@16.1.1':
     resolution: {integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==}
     engines: {node: '>= 10'}
@@ -4366,6 +4485,13 @@ packages:
     os: [linux]
     libc: [glibc]
 
+  '@next/swc-linux-arm64-gnu@16.2.1':
+    resolution: {integrity: sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
   '@next/swc-linux-arm64-musl@16.1.1':
     resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
     engines: {node: '>= 10'}
@@ -4380,6 +4506,13 @@ packages:
     os: [linux]
     libc: [musl]
 
+  '@next/swc-linux-arm64-musl@16.2.1':
+    resolution: {integrity: sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
   '@next/swc-linux-x64-gnu@16.1.1':
     resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
     engines: {node: '>= 10'}
@@ -4394,6 +4527,13 @@ packages:
     os: [linux]
     libc: [glibc]
 
+  '@next/swc-linux-x64-gnu@16.2.1':
+    resolution: {integrity: sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
   '@next/swc-linux-x64-musl@16.1.1':
     resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
     engines: {node: '>= 10'}
@@ -4408,6 +4548,13 @@ packages:
     os: [linux]
     libc: [musl]
 
+  '@next/swc-linux-x64-musl@16.2.1':
+    resolution: {integrity: sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
   '@next/swc-win32-arm64-msvc@16.1.1':
     resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
     engines: {node: '>= 10'}
@@ -4420,6 +4567,12 @@ packages:
     cpu: [arm64]
     os: [win32]
 
+  '@next/swc-win32-arm64-msvc@16.2.1':
+    resolution: {integrity: sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
   '@next/swc-win32-x64-msvc@16.1.1':
     resolution: {integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==}
     engines: {node: '>= 10'}
@@ -4432,6 +4585,12 @@ packages:
     cpu: [x64]
     os: [win32]
 
+  '@next/swc-win32-x64-msvc@16.2.1':
+    resolution: {integrity: sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
   '@noble/ciphers@1.3.0':
     resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
     engines: {node: ^14.21.3 || >=16}
@@ -6868,11 +7027,19 @@ packages:
   '@visual-json/core@0.1.1':
     resolution: {integrity: sha512-VLARpQnjJU1SnnYk1NiXScGfp2FyAwyZS2kA8hIJV5OzkaH2oS1dTHsaB8vOiyi14/L7jsbpA4g/EwzRWbxQmg==}
 
+  '@visual-json/core@0.3.1':
+    resolution: {integrity: sha512-rjRbcCbKBixuQpXqVPgzKl3Nfk0QLD1NJhuHittM0ASYETozxTsou4fL6g/IWz/BaYiAAjKnrkb8aasbzJGcvg==}
+
   '@visual-json/react@0.1.1':
     resolution: {integrity: sha512-KCpFYmBhH5F+xctfvxxVX7mVZK/iwvDwh/ITNkGPzEKXTIJQ16fau+hrG7m7QoTAOxvsdhHtHoCUiEjNHXboUQ==}
     peerDependencies:
       react: ^18.0.0 || ^19.0.0
 
+  '@visual-json/react@0.3.1':
+    resolution: {integrity: sha512-OoyKkhSs2MzS4embJZY3VPYUl8/gTn1BycWZskspHul/Sn4nfWjEUafWe6aWSeIKhuAw8HQwbw2krgTtuMM4Gg==}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0
+
   '@vitejs/plugin-react@5.1.4':
     resolution: {integrity: sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==}
     engines: {node: ^20.19.0 || >=22.12.0}
@@ -7425,6 +7592,11 @@ packages:
   base64-js@1.5.1:
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
 
+  baseline-browser-mapping@2.10.10:
+    resolution: {integrity: sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
   baseline-browser-mapping@2.9.14:
     resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
     hasBin: true
@@ -10477,6 +10649,11 @@ packages:
     peerDependencies:
       react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
+  lucide-react@1.6.0:
+    resolution: {integrity: sha512-YxLKVCOF5ZDI1AhKQE5IBYMY9y/Nr4NT15+7QEWpsTSVCdn4vmZhww+6BP76jWYjQx8rSz1Z+gGme1f+UycWEw==}
+    peerDependencies:
+      react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   lucide-svelte@0.500.0:
     resolution: {integrity: sha512-fdCJ4z9FuMoXk/ZPv4V5JhpMzQuEsoVNARXEPB0/zTX5AXA3l0x6J5sVUy9y6Rxxi3DEFpa5PmMwcXuO/ksOGw==}
     peerDependencies:
@@ -11002,6 +11179,27 @@ packages:
       sass:
         optional: true
 
+  next@16.2.1:
+    resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==}
+    engines: {node: '>=20.9.0'}
+    hasBin: true
+    peerDependencies:
+      '@opentelemetry/api': ^1.1.0
+      '@playwright/test': ^1.51.1
+      babel-plugin-react-compiler: '*'
+      react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+      react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+      sass: ^1.3.0
+    peerDependenciesMeta:
+      '@opentelemetry/api':
+        optional: true
+      '@playwright/test':
+        optional: true
+      babel-plugin-react-compiler:
+        optional: true
+      sass:
+        optional: true
+
   node-abi@3.89.0:
     resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==}
     engines: {node: '>=10'}
@@ -11747,6 +11945,12 @@ packages:
       react: ^18.0.0 || ^19.0.0
       react-dom: ^18.0.0 || ^19.0.0
 
+  react-resizable-panels@4.7.6:
+    resolution: {integrity: sha512-w3gbmUihfvH2Ho0iV1ULS2c/E/7HW/6g0GihogsIHjZf+JmmyVnKhryB3+I4JSxO8++uD3cKsSpOVTJV+GWEuA==}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+
   react-smooth@4.0.4:
     resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
     peerDependencies:
@@ -15332,7 +15536,6 @@ snapshots:
       - graphql
       - supports-color
       - utf-8-validate
-    optional: true
 
   '@expo/code-signing-certificates@0.0.6':
     dependencies:
@@ -15397,7 +15600,6 @@ snapshots:
     optionalDependencies:
       react: 19.2.4
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
-    optional: true
 
   '@expo/env@2.0.8':
     dependencies:
@@ -15467,7 +15669,7 @@ snapshots:
       postcss: 8.4.49
       resolve-from: 5.0.0
     optionalDependencies:
-      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
     transitivePeerDependencies:
       - bufferutil
       - supports-color
@@ -15548,7 +15750,7 @@ snapshots:
       '@expo/json-file': 10.0.8
       '@react-native/normalize-colors': 0.81.5
       debug: 4.4.3
-      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
       resolve-from: 5.0.0
       semver: 7.7.4
       xml2js: 0.6.0
@@ -15576,7 +15778,6 @@ snapshots:
       expo-font: 14.0.11(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
       react: 19.2.4
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
-    optional: true
 
   '@expo/ws-tunnel@1.0.6': {}
 
@@ -16223,6 +16424,8 @@ snapshots:
 
   '@next/env@16.1.6': {}
 
+  '@next/env@16.2.1': {}
+
   '@next/eslint-plugin-next@15.5.9':
     dependencies:
       fast-glob: 3.3.1
@@ -16240,48 +16443,72 @@ snapshots:
   '@next/swc-darwin-arm64@16.1.6':
     optional: true
 
+  '@next/swc-darwin-arm64@16.2.1':
+    optional: true
+
   '@next/swc-darwin-x64@16.1.1':
     optional: true
 
   '@next/swc-darwin-x64@16.1.6':
     optional: true
 
+  '@next/swc-darwin-x64@16.2.1':
+    optional: true
+
   '@next/swc-linux-arm64-gnu@16.1.1':
     optional: true
 
   '@next/swc-linux-arm64-gnu@16.1.6':
     optional: true
 
+  '@next/swc-linux-arm64-gnu@16.2.1':
+    optional: true
+
   '@next/swc-linux-arm64-musl@16.1.1':
     optional: true
 
   '@next/swc-linux-arm64-musl@16.1.6':
     optional: true
 
+  '@next/swc-linux-arm64-musl@16.2.1':
+    optional: true
+
   '@next/swc-linux-x64-gnu@16.1.1':
     optional: true
 
   '@next/swc-linux-x64-gnu@16.1.6':
     optional: true
 
+  '@next/swc-linux-x64-gnu@16.2.1':
+    optional: true
+
   '@next/swc-linux-x64-musl@16.1.1':
     optional: true
 
   '@next/swc-linux-x64-musl@16.1.6':
     optional: true
 
+  '@next/swc-linux-x64-musl@16.2.1':
+    optional: true
+
   '@next/swc-win32-arm64-msvc@16.1.1':
     optional: true
 
   '@next/swc-win32-arm64-msvc@16.1.6':
     optional: true
 
+  '@next/swc-win32-arm64-msvc@16.2.1':
+    optional: true
+
   '@next/swc-win32-x64-msvc@16.1.1':
     optional: true
 
   '@next/swc-win32-x64-msvc@16.1.6':
     optional: true
 
+  '@next/swc-win32-x64-msvc@16.2.1':
+    optional: true
+
   '@noble/ciphers@1.3.0': {}
 
   '@noble/curves@1.9.7':
@@ -19143,7 +19370,7 @@ snapshots:
   '@stripe/ui-extension-tools@0.0.1(@babel/core@7.29.0)(babel-jest@27.5.1(@babel/core@7.29.0))':
     dependencies:
       '@types/jest': 28.1.8
-      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
+      '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
       '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
       eslint: 8.57.1
       eslint-plugin-react: 7.37.5(eslint@8.57.1)
@@ -19812,7 +20039,7 @@ snapshots:
       '@types/node': 22.19.6
     optional: true
 
-  '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
+  '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
       '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
@@ -20060,11 +20287,18 @@ snapshots:
 
   '@visual-json/core@0.1.1': {}
 
+  '@visual-json/core@0.3.1': {}
+
   '@visual-json/react@0.1.1(react@19.2.3)':
     dependencies:
       '@visual-json/core': 0.1.1
       react: 19.2.3
 
+  '@visual-json/react@0.3.1(react@19.2.4)':
+    dependencies:
+      '@visual-json/core': 0.3.1
+      react: 19.2.4
+
   '@vitejs/plugin-react@5.1.4(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))':
     dependencies:
       '@babel/core': 7.29.0
@@ -20113,6 +20347,15 @@ snapshots:
       msw: 2.12.10(@types/node@22.19.6)(typescript@5.9.2)
       vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3)
 
+  '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
+    dependencies:
+      '@vitest/spy': 4.0.17
+      estree-walker: 3.0.3
+      magic-string: 0.30.21
+    optionalDependencies:
+      msw: 2.12.10(@types/node@22.19.6)(typescript@5.9.3)
+      vite: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
+
   '@vitest/mocker@4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))':
     dependencies:
       '@vitest/spy': 4.0.17
@@ -20752,7 +20995,7 @@ snapshots:
       resolve-from: 5.0.0
     optionalDependencies:
       '@babel/runtime': 7.28.6
-      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
+      expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
     transitivePeerDependencies:
       - '@babel/core'
       - supports-color
@@ -20787,6 +21030,8 @@ snapshots:
 
   base64-js@1.5.1: {}
 
+  baseline-browser-mapping@2.10.10: {}
+
   baseline-browser-mapping@2.9.14: {}
 
   bash-tool@1.3.14(ai@6.0.103(zod@4.3.6))(just-bash@2.9.6):
@@ -22239,7 +22484,6 @@ snapshots:
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
     transitivePeerDependencies:
       - supports-color
-    optional: true
 
   expo-clipboard@8.0.8(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
     dependencies:
@@ -22264,7 +22508,6 @@ snapshots:
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
     transitivePeerDependencies:
       - supports-color
-    optional: true
 
   expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)):
     dependencies:
@@ -22275,7 +22518,6 @@ snapshots:
     dependencies:
       expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
-    optional: true
 
   expo-font@14.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
     dependencies:
@@ -22290,7 +22532,6 @@ snapshots:
       fontfaceobserver: 2.3.0
       react: 19.2.4
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
-    optional: true
 
   expo-keep-awake@15.0.8(expo@54.0.33)(react@19.1.0):
     dependencies:
@@ -22301,7 +22542,6 @@ snapshots:
     dependencies:
       expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
       react: 19.2.4
-    optional: true
 
   expo-linking@8.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
     dependencies:
@@ -22343,7 +22583,6 @@ snapshots:
       invariant: 2.2.4
       react: 19.2.4
       react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
-    optional: true
 
   expo-router@6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.33)(react-dom@19.2.4(react@19.1.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.11.1(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
     dependencies:
@@ -22506,7 +22745,6 @@ snapshots:
       - graphql
       - supports-color
       - utf-8-validate
-    optional: true
 
   exponential-backoff@3.1.3: {}
 
@@ -22811,6 +23049,10 @@ snapshots:
     dependencies:
       next: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
 
+  geist@1.7.0(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+    dependencies:
+      next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+
   generator-function@2.0.1: {}
 
   gensync@1.0.0-beta.2: {}
@@ -24542,6 +24784,10 @@ snapshots:
     dependencies:
       react: 19.2.4
 
+  lucide-react@1.6.0(react@19.2.4):
+    dependencies:
+      react: 19.2.4
+
   lucide-svelte@0.500.0(svelte@5.53.5):
     dependencies:
       svelte: 5.53.5
@@ -25459,6 +25705,32 @@ snapshots:
       - '@babel/core'
       - babel-plugin-macros
 
+  next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+    dependencies:
+      '@next/env': 16.2.1
+      '@swc/helpers': 0.5.15
+      baseline-browser-mapping: 2.10.10
+      caniuse-lite: 1.0.30001764
+      postcss: 8.4.31
+      react: 19.2.4
+      react-dom: 19.2.4(react@19.2.4)
+      styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4)
+    optionalDependencies:
+      '@next/swc-darwin-arm64': 16.2.1
+      '@next/swc-darwin-x64': 16.2.1
+      '@next/swc-linux-arm64-gnu': 16.2.1
+      '@next/swc-linux-arm64-musl': 16.2.1
+      '@next/swc-linux-x64-gnu': 16.2.1
+      '@next/swc-linux-x64-musl': 16.2.1
+      '@next/swc-win32-arm64-msvc': 16.2.1
+      '@next/swc-win32-x64-msvc': 16.2.1
+      '@opentelemetry/api': 1.9.0
+      babel-plugin-react-compiler: 1.0.0
+      sharp: 0.34.5
+    transitivePeerDependencies:
+      - '@babel/core'
+      - babel-plugin-macros
+
   node-abi@3.89.0:
     dependencies:
       semver: 7.7.4
@@ -26511,6 +26783,11 @@ snapshots:
       react: 19.2.4
       react-dom: 19.2.4(react@19.2.4)
 
+  react-resizable-panels@4.7.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+    dependencies:
+      react: 19.2.4
+      react-dom: 19.2.4(react@19.2.4)
+
   react-smooth@4.0.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
     dependencies:
       fast-equals: 5.4.0
@@ -28647,7 +28924,7 @@ snapshots:
   vitest@4.0.17(@opentelemetry/api@1.9.0)(@types/node@22.19.6)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
     dependencies:
       '@vitest/expect': 4.0.17
-      '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.3))
+      '@vitest/mocker': 4.0.17(msw@2.12.10(@types/node@22.19.6)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
       '@vitest/pretty-format': 4.0.17
       '@vitest/runner': 4.0.17
       '@vitest/snapshot': 4.0.17
@@ -28807,7 +29084,7 @@ snapshots:
       '@webassemblyjs/ast': 1.14.1
       '@webassemblyjs/wasm-edit': 1.14.1
       '@webassemblyjs/wasm-parser': 1.14.1
-      acorn: 8.15.0
+      acorn: 8.16.0
       browserslist: 4.28.1
       chrome-trace-event: 1.0.4
       enhanced-resolve: 5.19.0

+ 194 - 0
skills/next/SKILL.md

@@ -0,0 +1,194 @@
+---
+name: next
+description: Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.
+---
+
+# @json-render/next
+
+Next.js renderer that converts JSON specs into full Next.js applications with routes, pages, layouts, metadata, and SSR support.
+
+## Quick Start
+
+```bash
+npm install @json-render/core @json-render/react @json-render/next
+```
+
+### 1. Define Your Spec
+
+```typescript
+// lib/spec.ts
+import type { NextAppSpec } from "@json-render/next";
+
+export const spec: NextAppSpec = {
+  metadata: {
+    title: { default: "My App", template: "%s | My App" },
+    description: "A json-render Next.js application",
+  },
+  layouts: {
+    main: {
+      root: "shell",
+      elements: {
+        shell: { type: "Container", props: {}, children: ["nav", "slot"] },
+        nav: { type: "NavBar", props: { links: [
+          { href: "/", label: "Home" },
+          { href: "/about", label: "About" },
+        ]}, children: [] },
+        slot: { type: "Slot", props: {}, children: [] },
+      },
+    },
+  },
+  routes: {
+    "/": {
+      layout: "main",
+      metadata: { title: "Home" },
+      page: {
+        root: "hero",
+        elements: {
+          hero: { type: "Card", props: { title: "Welcome" }, children: [] },
+        },
+      },
+    },
+    "/about": {
+      layout: "main",
+      metadata: { title: "About" },
+      page: {
+        root: "content",
+        elements: {
+          content: { type: "Card", props: { title: "About Us" }, children: [] },
+        },
+      },
+    },
+  },
+};
+```
+
+### 2. Create the App
+
+```typescript
+// lib/app.ts
+import { createNextApp } from "@json-render/next/server";
+import { spec } from "./spec";
+
+export const { Page, generateMetadata, generateStaticParams } = createNextApp({
+  spec,
+  loaders: {
+    // Server-side data loaders (optional)
+    loadPost: async ({ slug }) => {
+      const post = await getPost(slug as string);
+      return { post };
+    },
+  },
+});
+```
+
+### 3. Wire Up Route Files
+
+```tsx
+// app/[[...slug]]/page.tsx
+export { Page as default, generateMetadata, generateStaticParams } from "@/lib/app";
+```
+
+```tsx
+// app/[[...slug]]/layout.tsx
+import { NextAppProvider } from "@json-render/next";
+import { registry, handlers } from "@/lib/registry";
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+  return (
+    <html lang="en">
+      <body>
+        <NextAppProvider registry={registry} handlers={handlers}>
+          {children}
+        </NextAppProvider>
+      </body>
+    </html>
+  );
+}
+```
+
+## Key Concepts
+
+### NextAppSpec
+
+The top-level spec defines an entire Next.js application:
+
+- **metadata**: Root-level SEO metadata (title template, description, OpenGraph)
+- **layouts**: Reusable layout element trees (each must include a `Slot` component)
+- **routes**: Route definitions keyed by URL pattern
+- **state**: Global initial state shared across all routes
+
+### Route Patterns
+
+Routes use Next.js URL conventions:
+
+- `"/"` -- home page
+- `"/about"` -- static route
+- `"/blog/[slug]"` -- dynamic segment
+- `"/docs/[...path]"` -- catch-all segment
+- `"/settings/[[...path]]"` -- optional catch-all segment
+
+### Layouts
+
+Layouts wrap page content. Every layout MUST include a `Slot` component where page content will be rendered. Layouts are defined once in `spec.layouts` and referenced by routes via the `layout` field.
+
+### Built-in Components
+
+- **Slot**: Placeholder in layouts where page content is rendered
+- **Link**: Client-side navigation link (wraps `next/link`)
+
+### Built-in Actions
+
+- **setState**: Update state value. Params: `{ statePath, value }`
+- **pushState**: Append to array. Params: `{ statePath, value, clearStatePath? }`
+- **removeState**: Remove from array by index. Params: `{ statePath, index }`
+- **navigate**: Client-side navigation. Params: `{ href }`
+
+### Data Loaders
+
+Server-side async functions that run in the Server Component before rendering. Results are merged into the page's initial state.
+
+```typescript
+createNextApp({
+  spec,
+  loaders: {
+    loadPost: async ({ slug }) => {
+      const post = await db.post.findUnique({ where: { slug } });
+      return { post };
+    },
+  },
+});
+```
+
+### SSR
+
+Pages are server-rendered automatically. The `createNextApp` `Page` component is an async Server Component that:
+
+1. Matches the route from the spec
+2. Runs server-side data loaders
+3. Generates metadata
+4. Passes the resolved spec to the client renderer for hydration
+
+### Entry Points
+
+- `@json-render/next` -- Client components (NextAppProvider, PageRenderer, Link)
+- `@json-render/next/server` -- Server utilities (createNextApp, matchRoute, schema)
+
+## API Reference
+
+### Server Exports (`@json-render/next/server`)
+
+- `createNextApp(options)` -- Create Page, generateMetadata, generateStaticParams
+- `schema` -- Custom schema for Next.js apps (for AI catalog generation)
+- `matchRoute(spec, pathname)` -- Match a URL to a route spec
+- `resolveMetadata(spec, route)` -- Resolve metadata for a route
+- `slugToPath(slug)` -- Convert catch-all slug array to pathname
+- `collectStaticParams(spec)` -- Collect static params for all routes
+
+### Client Exports (`@json-render/next`)
+
+- `NextAppProvider` -- Context provider for registry and handlers
+- `PageRenderer` -- Renders a page spec with optional layout
+- `NextErrorBoundary` -- Error boundary component
+- `NextLoading` -- Loading state component
+- `NextNotFound` -- Not-found component
+- `Link` -- Built-in navigation component (wraps next/link)